Compare commits
2 Commits
5577407f97
...
354e897e55
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
354e897e55 | ||
|
|
b049dded72 |
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"sakunpanthi.gitea-integration"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"editor.fontSize": 18
|
||||||
|
}
|
||||||
BIN
CableCalc/icon.png
Normal file
|
After Width: | Height: | Size: 361 KiB |
481
CableCalc/index.php
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
<?php
|
||||||
|
// index.php - simple cable size calculator
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>CableCalc – Cable Size Calculator</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f4f4f4;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--text: #222;
|
||||||
|
--text-muted: #555;
|
||||||
|
--border: #ccc;
|
||||||
|
--accent: #0078d7;
|
||||||
|
--result-bg: #f0f7ff;
|
||||||
|
--result-border: #c5ddff;
|
||||||
|
--error-bg: #ffecec;
|
||||||
|
--error-border: #ffb3b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DARK MODE VARIABLES */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #111;
|
||||||
|
--card-bg: #1b1b1b;
|
||||||
|
--text: #eee;
|
||||||
|
--text-muted: #bbb;
|
||||||
|
--border: #444;
|
||||||
|
--accent: #3ea0ff;
|
||||||
|
--result-bg: #102233;
|
||||||
|
--result-border: #1a3a57;
|
||||||
|
--error-bg: #331111;
|
||||||
|
--error-border: #883333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* USER OVERRIDE – DARK */
|
||||||
|
body.dark {
|
||||||
|
--bg: #111;
|
||||||
|
--card-bg: #1b1b1b;
|
||||||
|
--text: #eee;
|
||||||
|
--text-muted: #bbb;
|
||||||
|
--border: #444;
|
||||||
|
--accent: #3ea0ff;
|
||||||
|
--result-bg: #102233;
|
||||||
|
--result-border: #1a3a57;
|
||||||
|
--error-bg: #331111;
|
||||||
|
--error-border: #883333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* USER OVERRIDE – LIGHT */
|
||||||
|
body.light {
|
||||||
|
--bg: #f4f4f4;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--text: #222;
|
||||||
|
--text-muted: #555;
|
||||||
|
--border: #ccc;
|
||||||
|
--accent: #0078d7;
|
||||||
|
--result-bg: #f0f7ff;
|
||||||
|
--result-border: #c5ddff;
|
||||||
|
--error-bg: #ffecec;
|
||||||
|
--error-border: #ffb3b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base styles */
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
transition: background 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 20px auto 40px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
padding: 20px 24px 28px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.20);
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||||
|
gap: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input,
|
||||||
|
.field select {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--text);
|
||||||
|
transition: background 0.3s, color 0.3s, border 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-options label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: none;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--result-bg);
|
||||||
|
border: 1px solid var(--result-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--error-bg);
|
||||||
|
border: 1px solid var(--error-border);
|
||||||
|
color: #ff8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes {
|
||||||
|
margin-top: 14px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
background: var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle button */
|
||||||
|
#modeToggle {
|
||||||
|
float: right;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>CableCalc <span class="badge">beta</span></h1>
|
||||||
|
<p style="font-size:0.9rem; color:#555;">
|
||||||
|
Enter your data – the calculator suggests the smallest copper cable size that keeps current
|
||||||
|
within a basic ampacity and voltage drop limit.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<!-- AC / DC -->
|
||||||
|
<div class="field">
|
||||||
|
<label for="acdc">AC / DC</label>
|
||||||
|
<select id="acdc">
|
||||||
|
<option value="AC">AC</option>
|
||||||
|
<option value="DC">DC</option>
|
||||||
|
</select>
|
||||||
|
<small>AC/DC is currently only shown for reference; calculation uses the same resistive model.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Voltage -->
|
||||||
|
<div class="field">
|
||||||
|
<label for="voltage">Voltage [V]</label>
|
||||||
|
<input type="number" id="voltage" min="0" step="0.1" value="230">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current / Power selector -->
|
||||||
|
<div class="field">
|
||||||
|
<label>What is known?</label>
|
||||||
|
<div class="inline-options">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="loadType" value="current" checked>
|
||||||
|
Current (A) known
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="loadType" value="power">
|
||||||
|
Power (W) known
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current input -->
|
||||||
|
<div class="field" id="field-current">
|
||||||
|
<label for="current">Current [A]</label>
|
||||||
|
<input type="number" id="current" min="0" step="0.01">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Power input -->
|
||||||
|
<div class="field" id="field-power" style="display:none;">
|
||||||
|
<label for="power">Power [W]</label>
|
||||||
|
<input type="number" id="power" min="0" step="1">
|
||||||
|
<small>Assumes cos φ / power factor = 1.0 for simplicity.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Length -->
|
||||||
|
<div class="field">
|
||||||
|
<label for="length">Cable length (one way)</label>
|
||||||
|
<input type="number" id="length" min="0" step="0.1" value="10">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Length unit -->
|
||||||
|
<div class="field">
|
||||||
|
<label for="lengthUnit">Length unit</label>
|
||||||
|
<select id="lengthUnit">
|
||||||
|
<option value="m">Meters</option>
|
||||||
|
<option value="ft">Feet</option>
|
||||||
|
</select>
|
||||||
|
<small>Voltage drop is calculated over the round-trip (out & back).</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Max voltage drop -->
|
||||||
|
<div class="field">
|
||||||
|
<label for="maxDrop">Max voltage drop [%]</label>
|
||||||
|
<input type="number" id="maxDrop" min="0.5" step="0.1" value="3.0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Output units -->
|
||||||
|
<div class="field">
|
||||||
|
<label for="unitSystem">Preferred output</label>
|
||||||
|
<select id="unitSystem">
|
||||||
|
<option value="mixed">AWG + mm²</option>
|
||||||
|
<option value="awg">AWG only</option>
|
||||||
|
<option value="mm2">mm² only</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" onclick="calculateCable()">Calculate cable size</button>
|
||||||
|
|
||||||
|
<div id="error" class="error" style="display:none;"></div>
|
||||||
|
<div id="result" class="result" style="display:none;"></div>
|
||||||
|
|
||||||
|
<div id="modeToggle" onclick="toggleMode()">🌓</div>
|
||||||
|
|
||||||
|
<div class="notes">
|
||||||
|
<strong>Notes & limitations:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Assumes copper conductors, single circuit, free air / light installation.</li>
|
||||||
|
<li>Uses generic ampacity values (conservative but not a substitute for local electrical codes).</li>
|
||||||
|
<li>For mains or critical installations always have a qualified electrician verify the result.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Cable data: copper, approx. resistance at 20°C and conservative ampacity in A.
|
||||||
|
// Values are generic and simplified – not a substitute for national standards.
|
||||||
|
const cableSizes = [
|
||||||
|
// AWG, mm², diameter(mm, bare conductor approx), R[ohm/km], ampacity[A]
|
||||||
|
{ awg: 18, mm2: 0.82, diameter: 1.02, r_km: 21.0, amp: 14 },
|
||||||
|
{ awg: 16, mm2: 1.31, diameter: 1.29, r_km: 13.3, amp: 18 },
|
||||||
|
{ awg: 14, mm2: 2.08, diameter: 1.63, r_km: 8.4, amp: 24 },
|
||||||
|
{ awg: 12, mm2: 3.31, diameter: 2.05, r_km: 5.3, amp: 32 },
|
||||||
|
{ awg: 10, mm2: 5.26, diameter: 2.59, r_km: 3.3, amp: 45 },
|
||||||
|
{ awg: 8, mm2: 8.37, diameter: 3.26, r_km: 2.1, amp: 60 },
|
||||||
|
{ awg: 6, mm2: 13.3, diameter: 4.11, r_km: 1.3, amp: 80 },
|
||||||
|
{ awg: 4, mm2: 21.1, diameter: 5.19, r_km: 0.8, amp: 105 },
|
||||||
|
{ awg: 2, mm2: 33.6, diameter: 6.54, r_km: 0.51, amp: 140 },
|
||||||
|
{ awg: 1, mm2: 42.4, diameter: 7.35, r_km: 0.40, amp: 165 },
|
||||||
|
{ awg: 0, mm2: 53.5, diameter: 8.25, r_km: 0.32, amp: 195 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Toggle current / power fields
|
||||||
|
document.querySelectorAll('input[name="loadType"]').forEach(r => {
|
||||||
|
r.addEventListener('change', function () {
|
||||||
|
const type = this.value;
|
||||||
|
document.getElementById('field-current').style.display = (type === 'current') ? 'block' : 'none';
|
||||||
|
document.getElementById('field-power').style.display = (type === 'power') ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
const err = document.getElementById('error');
|
||||||
|
err.textContent = msg;
|
||||||
|
err.style.display = 'block';
|
||||||
|
document.getElementById('result').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearError() {
|
||||||
|
const err = document.getElementById('error');
|
||||||
|
err.style.display = 'none';
|
||||||
|
err.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMode() {
|
||||||
|
if (document.body.classList.contains("dark")) {
|
||||||
|
document.body.classList.remove("dark");
|
||||||
|
document.body.classList.add("light");
|
||||||
|
localStorage.setItem("cablecalc-theme", "light");
|
||||||
|
} else if (document.body.classList.contains("light")) {
|
||||||
|
document.body.classList.remove("light");
|
||||||
|
document.body.classList.add("dark");
|
||||||
|
localStorage.setItem("cablecalc-theme", "dark");
|
||||||
|
} else {
|
||||||
|
// No override yet → force dark
|
||||||
|
document.body.classList.add("dark");
|
||||||
|
localStorage.setItem("cablecalc-theme", "dark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved preference
|
||||||
|
(function() {
|
||||||
|
const saved = localStorage.getItem("cablecalc-theme");
|
||||||
|
if (saved === "dark") document.body.classList.add("dark");
|
||||||
|
if (saved === "light") document.body.classList.add("light");
|
||||||
|
})();
|
||||||
|
|
||||||
|
function calculateCable() {
|
||||||
|
clearError();
|
||||||
|
const voltage = parseFloat(document.getElementById('voltage').value);
|
||||||
|
const lengthOneWay = parseFloat(document.getElementById('length').value);
|
||||||
|
const lengthUnit = document.getElementById('lengthUnit').value;
|
||||||
|
const maxDropPct = parseFloat(document.getElementById('maxDrop').value);
|
||||||
|
const unitSystem = document.getElementById('unitSystem').value;
|
||||||
|
const loadType = document.querySelector('input[name="loadType"]:checked').value;
|
||||||
|
|
||||||
|
if (isNaN(voltage) || voltage <= 0) {
|
||||||
|
showError('Please enter a valid voltage.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isNaN(lengthOneWay) || lengthOneWay <= 0) {
|
||||||
|
showError('Please enter a valid cable length.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isNaN(maxDropPct) || maxDropPct <= 0) {
|
||||||
|
showError('Please enter a valid maximum voltage drop percentage.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current;
|
||||||
|
if (loadType === 'current') {
|
||||||
|
current = parseFloat(document.getElementById('current').value);
|
||||||
|
if (isNaN(current) || current <= 0) {
|
||||||
|
showError('Please enter a valid current.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const power = parseFloat(document.getElementById('power').value);
|
||||||
|
if (isNaN(power) || power <= 0) {
|
||||||
|
showError('Please enter a valid power.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Power factor assumed 1.0
|
||||||
|
current = power / voltage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert length to meters (one way)
|
||||||
|
let length_m = lengthOneWay;
|
||||||
|
if (lengthUnit === 'ft') {
|
||||||
|
length_m = lengthOneWay * 0.3048;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total circuit length = out + back
|
||||||
|
const totalLength_m = length_m * 2;
|
||||||
|
|
||||||
|
const maxDrop_V = voltage * (maxDropPct / 100.0);
|
||||||
|
|
||||||
|
let chosen = null;
|
||||||
|
let worstCase = null; // largest size if nothing meets both criteria
|
||||||
|
|
||||||
|
for (let i = 0; i < cableSizes.length; i++) {
|
||||||
|
const c = cableSizes[i];
|
||||||
|
|
||||||
|
const rPerMeter = c.r_km / 1000.0; // ohm/m
|
||||||
|
const rTotal = rPerMeter * totalLength_m; // ohm
|
||||||
|
const vDrop = current * rTotal; // V
|
||||||
|
const vDropPct = (vDrop / voltage) * 100;
|
||||||
|
|
||||||
|
const okAmpacity = current <= c.amp;
|
||||||
|
const okDrop = vDrop <= maxDrop_V;
|
||||||
|
|
||||||
|
// Remember largest size for fallback
|
||||||
|
worstCase = {
|
||||||
|
cable: c,
|
||||||
|
vDrop: vDrop,
|
||||||
|
vDropPct: vDropPct,
|
||||||
|
okAmpacity: okAmpacity,
|
||||||
|
okDrop: okDrop
|
||||||
|
};
|
||||||
|
|
||||||
|
if (okAmpacity && okDrop) {
|
||||||
|
chosen = {
|
||||||
|
cable: c,
|
||||||
|
vDrop: vDrop,
|
||||||
|
vDropPct: vDropPct
|
||||||
|
};
|
||||||
|
break; // smallest that satisfies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = document.getElementById('result');
|
||||||
|
res.style.display = 'block';
|
||||||
|
|
||||||
|
if (!chosen && worstCase) {
|
||||||
|
const c = worstCase.cable;
|
||||||
|
const vDrop = worstCase.vDrop;
|
||||||
|
const vDropPct = worstCase.vDropPct;
|
||||||
|
|
||||||
|
res.innerHTML = `
|
||||||
|
<strong>No cable size in the built-in table fully meets the limits.</strong><br><br>
|
||||||
|
Suggested <em>minimum</em> size (table maximum):<br>
|
||||||
|
AWG ${c.awg}, approx. ${c.mm2.toFixed(1)} mm², conductor diameter ≈ ${c.diameter.toFixed(2)} mm<br>
|
||||||
|
Ampacity (approx.): ${c.amp} A<br>
|
||||||
|
Estimated voltage drop: ${vDrop.toFixed(2)} V (${vDropPct.toFixed(2)} %)<br><br>
|
||||||
|
<span style="color:#a40000;">Warning:</span> Either the desired voltage drop or ampacity is exceeded.
|
||||||
|
Consider using a larger cable than listed here or shortening the run.
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chosen) {
|
||||||
|
showError('No valid cable size found. Please check inputs.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = chosen.cable;
|
||||||
|
const vDrop = chosen.vDrop;
|
||||||
|
const vDropPct = chosen.vDropPct;
|
||||||
|
|
||||||
|
let mainLine = '';
|
||||||
|
if (unitSystem === 'awg') {
|
||||||
|
mainLine = `Recommended cable size: <strong>AWG ${c.awg}</strong>`;
|
||||||
|
} else if (unitSystem === 'mm2') {
|
||||||
|
mainLine = `Recommended cable size: <strong>${c.mm2.toFixed(1)} mm²</strong>`;
|
||||||
|
} else {
|
||||||
|
mainLine = `Recommended cable size: <strong>AWG ${c.awg} (~${c.mm2.toFixed(1)} mm²)</strong>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.innerHTML = `
|
||||||
|
${mainLine}<br>
|
||||||
|
Conductor diameter (approx.): <strong>${c.diameter.toFixed(2)} mm</strong><br>
|
||||||
|
Ampacity (approx.): <strong>${c.amp} A</strong><br>
|
||||||
|
Estimated voltage drop over ${length_m.toFixed(1)} m one-way (round-trip ${totalLength_m.toFixed(1)} m):<br>
|
||||||
|
<strong>${vDrop.toFixed(2)} V (${vDropPct.toFixed(2)} %)</strong>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
bibel-pwa/data.csv
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
Thema,Unterthema,Kommentar,Bibelstelle,Bibel-Verweis,Bibeltext,Schluesselwoerter
|
||||||
|
Zukunft,"Die aktuelle Weltlage und die Einstellung der Menschen deuten darauf hin, dass große Veränderungen bevorstehen","Die aktuelle Weltlage und die Einstellung der Menschen deuten darauf hin, dass große Veränderungen bevorstehen",Mat. 24:3,Mat. 24:3,"Als er später auf dem Ölberg saß, kamen die Jünger allein zu ihm und wollten wissen: „Sag uns: Wann wird das passieren und an welchem Zeichen wird man deine Gegenwart und den Abschluss des Weltsystems erkennen?“",Als;er;später;auf
|
||||||
|
Zukunft,"Die aktuelle Weltlage und die Einstellung der Menschen deuten darauf hin, dass große Veränderungen bevorstehen","Die aktuelle Weltlage und die Einstellung der Menschen deuten darauf hin, dass große Veränderungen bevorstehen","2. Tim. 3:1, 13","2. Tim. 3:1, 13","Das aber sollst du wissen: In den letzten Tagen werden kritische Zeiten herrschen, mit denen man schwer fertig wird. Doch schlechte Menschen und Betrüger werden es immer schlimmer treiben. Sie werden irreführen und irregeführt werden.",Das;aber;sollst;du
|
||||||
|
Zukunft,"Die aktuelle Weltlage und die Einstellung der Menschen deuten darauf hin, dass große Veränderungen bevorstehen","Die aktuelle Weltlage und die Einstellung der Menschen deuten darauf hin, dass große Veränderungen bevorstehen",2. Pet. 3:3,2. Pet. 3:3,Ihr müsst vor allem Folgendes wissen: In den letzten Tagen werden Spötter kommen und spotten. Sie werden nach ihren eigenen Wünschen vorgehen.,Ihr;müsst;vor;allem
|
||||||
|
Zukunft,"Die aktuelle Weltlage und die Einstellung der Menschen deuten darauf hin, dass große Veränderungen bevorstehen","Die aktuelle Weltlage und die Einstellung der Menschen deuten darauf hin, dass große Veränderungen bevorstehen",2. Tim. 3:1–5,2. Tim. 3:1–5,"Das aber sollst du wissen: In den letzten Tagen werden kritische Zeiten herrschen, mit denen man schwer fertig wird. Denn die Menschen werden sich selbst und das Geld lieben. Sie werden angeberisch und überheblich sein, über Gott und Menschen lästern, nicht auf die Eltern hören, undankbar sein und nicht loyal. Sie werden lieblos sein, nicht kompromissbereit, verleumderisch, unbeherrscht und brutal und werden das Gute nicht lieben. Sie werden Verräter sein, eigensinnig, aufgeblasen vor Stolz und werden das Vergnügen lieben statt Gott. Nach außen hin wird es so aussehen, als hätten sie Gottesfurcht, aber die Kraft dahinter zeigt sich in ihrem Leben nicht. Von solchen Menschen wende dich ab!",Das;aber;sollst;du
|
||||||
|
Zukunft,"Die aktuelle Weltlage und die Einstellung der Menschen deuten darauf hin, dass große Veränderungen bevorstehen","Die aktuelle Weltlage und die Einstellung der Menschen deuten darauf hin, dass große Veränderungen bevorstehen","Luk. 21:10, 11","Luk. 21:10, 11",Ein Volk wird sich gegen das andere erheben und ein Land gegen das andere. Es wird große Erdbeben geben und in einer Gegend nach der anderen Lebensmittelknappheit und Seuchen. Auch wird es erschreckende Anblicke geben und vom Himmel aus große Zeichen.,Ein;Volk;wird;sich
|
||||||
|
Zukunft,Die Erde wird nie zerstört werden,Die Erde wird nie zerstört werden,Pred. 1:4,Pred. 1:4,"Eine Generation geht, eine Generation kommt, aber die Erde bleibt für immer.",Eine;Generation;geht;eine
|
||||||
|
Zukunft,Die Erde wird nie zerstört werden,Die Erde wird nie zerstört werden,Jes. 45:18,Jes. 45:18,"Denn Folgendes sagt Jehova, der Schöpfer des Himmels, der wahre Gott, der die Erde formte, ihr Erschaffer, der ihr festen Bestand gab, der sie nicht umsonst erschuf, sondern sie formte, damit sie bewohnt wird: „Ich bin Jehova und sonst gibt es keinen.“",Denn;Folgendes;sagt;Jehova
|
||||||
|
Zukunft,Die Erde wird nie zerstört werden,Die Erde wird nie zerstört werden,Ps. 104:5,Ps. 104:5,Er hat die Erde auf ihren Fundamenten fest verankert. Für immer und ewig wird sie nicht von der Stelle gerückt.,Er;hat;die;Erde
|
||||||
|
Zukunft,Die Umweltzerstörung wird vollständig rückgängig gemacht,Die Umweltzerstörung wird vollständig rückgängig gemacht,Ps. 104:30,Ps. 104:30,"Schickst du deinen Geist, werden sie erschaffen, und du erneuerst die Oberfläche des Erdbodens.",Schickst;du;deinen;Geist
|
||||||
|
Zukunft,Die Umweltzerstörung wird vollständig rückgängig gemacht,Die Umweltzerstörung wird vollständig rückgängig gemacht,"Jes. 35:1, 6, 7","Jes. 35:1, 6, 7","Die Wildnis und das trockene Land werden jubeln, und die Wüstenebene wird sich freuen und blühen wie der Safran. Zu jener Zeit wird der Lahme springen wie ein Hirsch und die Zunge des Stummen wird jubeln. Denn in der Wildnis wird Wasser hervorsprudeln und in der Wüstenebene werden Bäche fließen. Der sonnenverbrannte Boden wird sich in einen Schilfteich verwandeln und der durstige Boden in Wasserquellen. Wo die Schakale hausten und lagerten, werden grünes Gras, Schilfrohr und Papyrus wachsen.",Die;Wildnis;und;das
|
||||||
|
Zukunft,Die Umweltzerstörung wird vollständig rückgängig gemacht,Die Umweltzerstörung wird vollständig rückgängig gemacht,Jes. 11:6–9,Jes. 11:6–9,"Der Wolf wird mit dem Lamm wohnen, der Leopard wird sich neben dem Böckchen niederlegen, ein Kalb, ein junger Löwe und ein Masttier werden zusammen sein; ein kleiner Junge wird sie hüten. Kühe und Bären werden miteinander weiden, ihre Jungen werden beieinander liegen, und der Löwe wird Stroh fressen wie das Rind. Ein Säugling wird am Schlupfloch der Kobra spielen und ein entwöhntes Kind wird seine Hand in das Loch einer Giftschlange legen. Niemand wird Böses tun oder Schaden anrichten auf meinem ganzen heiligen Berg, denn die Erde wird gewiss mit der Erkenntnis Jehovas erfüllt sein, so wie das Wasser den Meeresboden bedeckt.",Der;Wolf;wird;mit
|
||||||
|
Zukunft,Alle Menschen werden völlig gesund sein,Alle Menschen werden völlig gesund sein,Jes. 33:24,Jes. 33:24,"Und kein Bewohner wird sagen: „Ich bin krank.“ Dem Volk, das in dem Land wohnt, wird sein Vergehen verziehen werden.",Und;kein;Bewohner;wird
|
||||||
|
Zukunft,Alle Menschen werden völlig gesund sein,Alle Menschen werden völlig gesund sein,"Jes. 35:5, 6","Jes. 35:5, 6",Dann werden die Augen der Blinden geöffnet und die Ohren der Tauben werden geöffnet. Dann wird der Lahme springen wie ein Hirsch und die Zunge des Stummen wird jubeln. In der Wildnis werden Wasser hervorsprudeln und Ströme in der Wüste.,Dann;werden;die;Augen
|
||||||
|
Zukunft,Alle Menschen werden völlig gesund sein,Alle Menschen werden völlig gesund sein,Jes. 65:21–22,Jes. 65:21–22,"Sie werden Häuser bauen und sie bewohnen und sie werden Weingärten anlegen und deren Ertrag essen. Sie werden nicht bauen, damit es ein anderer bewohnt, und nicht pflanzen, damit es ein anderer isst. Denn mein Volk wird so lange leben wie ein Baum, und meine Auserwählten werden das, was ihre Hände leisten, in vollen Zügen genießen.",Sie;werden;Häuser;bauen
|
||||||
|
Zukunft,Alle Menschen werden völlig gesund sein,Alle Menschen werden völlig gesund sein,Offb. 21:4,Offb. 21:4,"Und er wird jede Träne von ihren Augen abwischen, und den Tod wird es nicht mehr geben. Auch wird es weder Trauer noch Aufschrei noch Schmerz mehr geben. Was früher war, ist vorbei.",Und;er;wird;jede
|
||||||
|
Zukunft,Menschen können für immer auf der Erde leben,Menschen können für immer auf der Erde leben,Ps. 37:29,Ps. 37:29,Die Gerechten werden die Erde besitzen und für immer auf ihr leben.,Die;Gerechten;werden;die
|
||||||
|
Zukunft,Menschen können für immer auf der Erde leben,Menschen können für immer auf der Erde leben,Ps. 119:165,Ps. 119:165,"Großen Frieden haben die, die dein Gesetz lieben, und nichts wird sie zu Fall bringen.",Großen;Frieden;haben;die
|
||||||
|
Zukunft,Menschen können für immer auf der Erde leben,Menschen können für immer auf der Erde leben,Offb. 21:4,Offb. 21:4,"Und er wird jede Träne von ihren Augen abwischen, und den Tod wird es nicht mehr geben. Auch wird es weder Trauer noch Aufschrei noch Schmerz mehr geben. Was früher war, ist vorbei.",Und;er;wird;jede
|
||||||
|
Zukunft,Menschen können für immer auf der Erde leben,Menschen können für immer auf der Erde leben,Joh. 3:16,Joh. 3:16,"Denn Gott hat die Welt so sehr geliebt, dass er seinen einziggezeugten Sohn gab, damit jeder, der Glauben an ihn ausübt, nicht vernichtet wird, sondern ewiges Leben hat.",Denn;Gott;hat;die
|
||||||
|
Familie,Ein Ehemann sollte seine Frau lieben wie sich selbst,Ein Ehemann sollte seine Frau lieben wie sich selbst,Eph. 5:33,Eph. 5:33,Auf jeden Fall soll jeder von euch seine Frau lieben wie sich selbst. Andererseits soll die Frau großen Respekt vor ihrem Mann haben.,Auf;jeden;Fall;soll
|
||||||
|
Familie,Ein Ehemann sollte seine Frau lieben wie sich selbst,Ein Ehemann sollte seine Frau lieben wie sich selbst,Kol. 3:19,Kol. 3:19,"Ihr Männer, liebt eure Frauen weiterhin, und seid nicht wütend auf sie.",Ihr;Männer;liebt;eure
|
||||||
|
Familie,Eine Ehefrau sollte ihren Mann von Herzen respektieren,Eine Ehefrau sollte ihren Mann von Herzen respektieren,Eph. 5:33,Eph. 5:33,Auf jeden Fall soll jeder von euch seine Frau lieben wie sich selbst. Andererseits soll die Frau großen Respekt vor ihrem Mann haben.,Auf;jeden;Fall;soll
|
||||||
|
Familie,Eine Ehefrau sollte ihren Mann von Herzen respektieren,Eine Ehefrau sollte ihren Mann von Herzen respektieren,1. Pet. 3:1,1. Pet. 3:1,"Ebenso sollt ihr Frauen euch euren Männern unterordnen, damit die, die dem Wort Gottes nicht gehorchen, durch euer Verhalten ohne ein Wort gewonnen werden.",Ebenso;sollt;ihr;Frauen
|
||||||
|
Familie,Mann und Frau sollten einander treu sein,Mann und Frau sollten einander treu sein,Mal. 2:16,Mal. 2:16,"Denn ich hasse Ehescheidung“, sagt Jehova, der Gott Israels, „und den, der gewalttätig ist.“ Achtet auf euren Geist und handelt nicht treulos!",Denn;ich;hasse;Ehescheidung
|
||||||
|
Familie,Mann und Frau sollten einander treu sein,Mann und Frau sollten einander treu sein,Heb. 13:4,Heb. 13:4,"Die Ehe soll von allen in Ehren gehalten werden und das Ehebett soll unbefleckt sein; denn Gott wird diejenigen richten, die sexuell unmoralisch handeln und Ehebruch begehen.",Die;Ehe;soll;von
|
||||||
|
Familie,"Es wirkt sich auf Kinder gut aus, wenn sie ihre Eltern respektieren und auf sie hören","Es wirkt sich auf Kinder gut aus, wenn sie ihre Eltern respektieren und auf sie hören","Spr. 1:8, 9","Spr. 1:8, 9","Mein Sohn, hör auf die Erziehung deines Vaters und lehne die Anleitung deiner Mutter nicht ab. Sie sind ein hübscher Kranz für deinen Kopf und ein schöner Schmuck für deinen Hals.",Mein;Sohn;hör;auf
|
||||||
|
Familie,"Es wirkt sich auf Kinder gut aus, wenn sie ihre Eltern respektieren und auf sie hören","Es wirkt sich auf Kinder gut aus, wenn sie ihre Eltern respektieren und auf sie hören",Eph. 6:1–3,Eph. 6:1–3,"Ihr Kinder, gehorcht euren Eltern, denn das ist richtig vor dem Herrn. ""Ehre deinen Vater und deine Mutter"" ist das erste Gebot mit einer Verheißung, damit es dir gut geht und du lange auf der Erde lebst.",Ihr;Kinder;gehorcht;euren
|
||||||
|
Familie,"Es wirkt sich auf Kinder gut aus, wenn sie ihre Eltern respektieren und auf sie hören","Es wirkt sich auf Kinder gut aus, wenn sie ihre Eltern respektieren und auf sie hören",Kol. 3:20,Kol. 3:20,"Ihr Kinder, gehorcht euren Eltern in allem, denn das ist dem Herrn wohlgefällig.",Ihr;Kinder;gehorcht;euren
|
||||||
|
Gott,Gott hat einen Namen,Gott hat einen Namen,Ps. 83:18,Ps. 83:18,"Die Menschen sollen wissen, dass du, dessen Name Jehova ist, du allein der Höchste bist über die ganze Erde.",Die;Menschen;sollen;wissen
|
||||||
|
Gott,Gott hat einen Namen,Gott hat einen Namen,2. Mo. 3:15,2. Mo. 3:15,"Jehova, der Gott eurer Vorfahren – der Gott Abrahams, Isaaks und Jakobs – hat mich zu euch gesandt. Das ist mein Name für immer, und so soll man mich von Generation zu Generation in Erinnerung behalten.",Jehova;der;Gott;eurer
|
||||||
|
Gott,Gott kommuniziert mit uns,Gott kommuniziert mit uns,"2. Tim. 3:16, 17","2. Tim. 3:16, 17","Die ganze heilige Schrift ist von Gott inspiriert und nützlich zum Lehren, zum Zurechtweisen, zum Richtigstellen und zum Erziehen in der Gerechtigkeit, damit der Mensch Gottes völlig fähig ist und für jedes gute Werk vollkommen ausgerüstet.",Die;ganze;heilige;Schrift
|
||||||
|
Gott,Gott kommuniziert mit uns,Gott kommuniziert mit uns,"Heb. 1:1, 2","Heb. 1:1, 2","Vor langer Zeit hat Gott viele Male und auf viele Weisen durch die Propheten zu unseren Vorfahren gesprochen. Jetzt, am Ende dieser Tage, hat er durch seinen Sohn zu uns gesprochen, den er zum Erben von allem eingesetzt hat und durch den er die Systeme der Dinge erschaffen hat.",Vor;langer;Zeit;hat
|
||||||
|
Gott,Gott ist fair und hat keine Vorurteile,Gott ist fair und hat keine Vorurteile,5. Mo. 10:17,5. Mo. 10:17,"Denn Jehova, euer Gott, ist der Gott der Götter und der Herr der Herren, ein großer, mächtiger und furchteinflößender Gott, der niemand bevorzugt und kein Bestechungsgeschenk annimmt.",Denn;Jehova;euer;Gott
|
||||||
|
Gott,Gott ist fair und hat keine Vorurteile,Gott ist fair und hat keine Vorurteile,Apg. 10:34,Apg. 10:34,"Jetzt begreife ich wirklich, dass Gott nicht parteiisch ist, sondern jeden aus allen Nationen annimmt, der ihn fürchtet und das Richtige tut.",Jetzt;begreife;ich;wirklich
|
||||||
|
Gott,Gott möchte uns helfen,Gott möchte uns helfen,Ps. 46:1,Ps. 46:1,"Gott ist unsere Zuflucht und Stärke, eine Hilfe, die in der Not leicht zu finden ist.",Gott;ist;unsere;Zuflucht
|
||||||
|
Gott,Gott möchte uns helfen,Gott möchte uns helfen,1. Pet. 5:7,1. Pet. 5:7,"Werft alle eure Sorgen auf ihn, denn er kümmert sich um euch.",Werft;alle;eure;Sorgen
|
||||||
|
Gebet,"Gott wünscht sich, dass wir zu ihm beten","Gott wünscht sich, dass wir zu ihm beten",Ps. 62:8,Ps. 62:8,"Vertraut auf ihn zu allen Zeiten, o Volk. Vor ihm schüttet euer Herz aus. Gott ist für uns eine Zuflucht.",Vertraut;auf;ihn;zu
|
||||||
|
Gebet,"Gott wünscht sich, dass wir zu ihm beten","Gott wünscht sich, dass wir zu ihm beten",Phil. 4:6,Phil. 4:6,"Macht euch um nichts Sorgen, sondern lasst Gott in allem durch Gebet und Flehen zusammen mit Dank wissen, was eure Bitten sind.",Macht;euch;um;nichts
|
||||||
|
Gebet,"Aus der Bibel erfahren wir, wie man beten sollte","Aus der Bibel erfahren wir, wie man beten sollte",Mat. 6:7–13,Mat. 6:7–13,"Wenn ihr betet, plappert nicht wie die Leute. So sollt ihr beten: Unser Vater im Himmel, dein Name werde geheiligt. Dein Königreich komme. Dein Wille geschehe wie im Himmel so auf der Erde. Gib uns heute unser tägliches Brot. Vergib uns unsere Schulden, wie auch wir vergeben haben. Bring uns nicht in Versuchung, sondern befreie uns vom Bösen.",Wenn;ihr;betet;plappert
|
||||||
|
Gebet,"Aus der Bibel erfahren wir, wie man beten sollte","Aus der Bibel erfahren wir, wie man beten sollte",Luk. 11:2–4,Luk. 11:2–4,"Wenn ihr betet, sagt: Vater, dein Name werde geheiligt. Dein Königreich komme. Gib uns täglich das Brot, das wir brauchen. Vergib uns unsere Sünden, denn auch wir vergeben jedem, der uns schuldig ist, und bring uns nicht in Versuchung.",Wenn;ihr;betet;sagt
|
||||||
|
Gebet,Wir sollten oft beten,Wir sollten oft beten,"Mat. 7:7, 8","Mat. 7:7, 8","Bittet, und es wird euch gegeben; sucht, und ihr werdet finden; klopft an, und es wird euch geöffnet. Denn wer bittet, erhält; wer sucht, findet; wer anklopft, dem wird geöffnet.",Bittet;und;es;wird
|
||||||
|
Gebet,Wir sollten oft beten,Wir sollten oft beten,1. Thess. 5:17,1. Thess. 5:17,Betet unablässig.,Betet;unablässig
|
||||||
|
Jesus,Jesus war ein außergewöhnlicher Lehrer; sein Rat funktioniert immer,Jesus war ein außergewöhnlicher Lehrer; sein Rat funktioniert immer,"Mat. 6:14, 15","Mat. 6:14, 15","Denn wenn ihr den Menschen ihre Verfehlungen vergebt, wird euer himmlischer Vater euch ebenfalls vergeben; wenn ihr aber den Menschen ihre Verfehlungen nicht vergebt, wird euer Vater auch euch nicht vergeben.",Denn;wenn;ihr;den
|
||||||
|
Jesus,Jesus war ein außergewöhnlicher Lehrer; sein Rat funktioniert immer,Jesus war ein außergewöhnlicher Lehrer; sein Rat funktioniert immer,Joh. 13:15,Joh. 13:15,"Ich habe euch ein Beispiel gegeben, damit auch ihr so handelt, wie ich an euch gehandelt habe.",Ich;habe;euch;ein
|
||||||
|
Jesus,"Jesus hat Ereignisse vorausgesagt, die wir heute beobachten können","Jesus hat Ereignisse vorausgesagt, die wir heute beobachten können",Mat. 24:3,Mat. 24:3,"Als er auf dem Ölberg saß, fragten ihn seine Jünger: 'Sag uns, wann wird das geschehen und was wird das Zeichen deiner Gegenwart und des Abschlusses des Systems sein?'.",Als;er;auf;dem
|
||||||
|
Jesus,"Jesus hat Ereignisse vorausgesagt, die wir heute beobachten können","Jesus hat Ereignisse vorausgesagt, die wir heute beobachten können","Luk. 21:10, 11","Luk. 21:10, 11","Ein Volk wird sich gegen das andere erheben und ein Königreich gegen das andere, und es wird große Erdbeben, Seuchen und Hungersnöte geben, und erschreckende Anblicke vom Himmel.",Ein;Volk;wird;sich
|
||||||
|
Jesus,Jesus ist Gottes Sohn,Jesus ist Gottes Sohn,Mat. 16:16,Mat. 16:16,"Du bist der Christus, der Sohn des lebendigen Gottes.",Du;bist;der;Christus
|
||||||
|
Jesus,Jesus ist Gottes Sohn,Jesus ist Gottes Sohn,Mk. 1:1,Mk. 1:1,"Der Anfang der guten Botschaft über Jesus Christus, den Sohn Gottes.",Der;Anfang;der;guten
|
||||||
|
Jesus,Jesus ist nicht der allmächtige Gott,Jesus ist nicht der allmächtige Gott,Joh. 14:28,Joh. 14:28,"Ihr habt gehört, dass ich euch gesagt habe: Ich gehe weg und komme wieder zu euch. Wenn ihr mich liebtet, würdet ihr euch freuen, dass ich zum Vater gehe, denn der Vater ist größer als ich.",Ihr;habt;gehört;dass
|
||||||
|
Jesus,Jesus ist nicht der allmächtige Gott,Jesus ist nicht der allmächtige Gott,1. Kor. 11:3,1. Kor. 11:3,"Ich möchte, dass ihr wisst: Der Christus ist das Haupt jedes Mannes, der Mann ist das Haupt der Frau, und Gott ist das Haupt des Christus.",Ich;möchte;dass;ihr
|
||||||
|
Reich Gottes,Das Reich Gottes ist eine echte Regierung im Himmel,Das Reich Gottes ist eine echte Regierung im Himmel,Dan. 2:44,Dan. 2:44,"Zur Zeit dieser Könige wird der Gott des Himmels ein Königreich errichten, das nie zerstört wird. Dieses Königreich wird all diese Königreiche zermalmen und vernichten, und selbst wird es für immer bestehen.",Zur;Zeit;dieser;Könige
|
||||||
|
Reich Gottes,Das Reich Gottes ist eine echte Regierung im Himmel,Das Reich Gottes ist eine echte Regierung im Himmel,Ps. 103:19,Ps. 103:19,"Jehova hat seinen Thron im Himmel gegründet, und seine Herrschaft herrscht über alles.",Jehova;hat;seinen;Thron
|
||||||
|
Reich Gottes,Gottes Reich wird alle Regierungen auf der Erde ersetzen,Gottes Reich wird alle Regierungen auf der Erde ersetzen,Ps. 2:7–9,Ps. 2:7–9,"Er sagte zu mir: 'Du bist mein Sohn; heute bin ich dein Vater geworden. Fordere mich, und ich gebe dir die Nationen zum Erbe und die Enden der Erde zum Besitz. Du wirst sie mit eisernem Zepter zerschmettern und wie ein Töpfergefäß zerschmeißen.'",Er;sagte;zu;mir
|
||||||
|
Reich Gottes,Gottes Reich wird alle Regierungen auf der Erde ersetzen,Gottes Reich wird alle Regierungen auf der Erde ersetzen,Offb. 11:15,Offb. 11:15,"Der siebte Engel blies die Trompete, und es wurden laute Stimmen im Himmel gehört: 'Das Königreich der Welt ist das Königreich unseres Herrn und seines Christus geworden, und er wird für immer und ewig herrschen.'",Der;siebte;Engel;blies
|
||||||
|
Reich Gottes,Nur Gottes Reich kann die Probleme der Menschheit lösen,Nur Gottes Reich kann die Probleme der Menschheit lösen,"Ps. 37:10, 11","Ps. 37:10, 11","Nur noch eine kurze Weile, und der Böse wird nicht mehr; aber die Sanftmütigen werden die Erde erben und sich über großen Frieden freuen.",Nur;noch;eine;kurze
|
||||||
|
Reich Gottes,Nur Gottes Reich kann die Probleme der Menschheit lösen,Nur Gottes Reich kann die Probleme der Menschheit lösen,"Mi. 4:3, 4","Mi. 4:3, 4","Er wird zwischen vielen Völkern Recht sprechen. Sie werden ihre Schwerter zu Pflugscharen schmieden und ihre Speere zu Winzermessern. Sie werden keinen Krieg mehr lernen. Jeder wird unter seinem Weinstock und seinem Feigenbaum sitzen, und niemand wird sie aufschrecken.",Er;wird;zwischen;vielen
|
||||||
|
Leid,"Gott ist nicht schuld, wenn wir leiden","Gott ist nicht schuld, wenn wir leiden",5. Mo. 32:4,5. Mo. 32:4,"Er ist der Fels, vollkommen sind seine Werke, denn alle seine Wege sind gerecht. Ein treuer Gott, der nie Unrecht tut; er ist gerecht und aufrichtig.",Er;ist;der;Fels
|
||||||
|
Leid,"Gott ist nicht schuld, wenn wir leiden","Gott ist nicht schuld, wenn wir leiden",Jak. 1:13,Jak. 1:13,"Wenn jemand in Versuchung gerät, soll er nicht sagen: 'Ich werde von Gott versucht.' Denn Gott kann nicht von Übel versucht werden, noch versucht er selbst jemanden.",Wenn;jemand;in;Versuchung
|
||||||
|
Leid,Diese Welt wird vom Teufel beherrscht,Diese Welt wird vom Teufel beherrscht,"Luk. 4:5, 6","Luk. 4:5, 6","Der Teufel zeigte ihm alle Königreiche der bewohnten Erde und sagte: 'Dir will ich all diese Autorität und Herrlichkeit geben, denn sie ist mir übergeben und ich gebe sie, wem ich will.'",Der;Teufel;zeigte;ihm
|
||||||
|
Leid,Diese Welt wird vom Teufel beherrscht,Diese Welt wird vom Teufel beherrscht,1. Joh. 5:19,1. Joh. 5:19,Die ganze Welt liegt in der Macht des Bösen.,Die;ganze;Welt;liegt
|
||||||
|
Leid,"Gott sieht es, wenn wir leiden, und möchte uns helfen","Gott sieht es, wenn wir leiden, und möchte uns helfen",Ps. 34:17–19,Ps. 34:17–19,"Der Gerechte ruft, und Jehova hört; aus all ihren Bedrängnissen rettet er sie. Jehova ist nahe denen, die gebrochenen Herzens sind, und die zerschlagenen Geistes sind, rettet er.",Der;Gerechte;ruft;und
|
||||||
|
Leid,"Gott sieht es, wenn wir leiden, und möchte uns helfen","Gott sieht es, wenn wir leiden, und möchte uns helfen",2. Chr. 16:9,2. Chr. 16:9,"Die Augen Jehovas durchstreifen die ganze Erde, um die zu stärken, die von Herzen loyal zu ihm sind.",Die;Augen;Jehovas;durchstreifen
|
||||||
|
Leid,Gott wird allem Leid bald ein Ende machen,Gott wird allem Leid bald ein Ende machen,Jes. 65:17,Jes. 65:17,"Siehe, ich schaffe einen neuen Himmel und eine neue Erde; an das Frühere wird man sich nicht erinnern noch wird es in den Sinn kommen.",Siehe;ich;schaffe;einen
|
||||||
|
Leid,Gott wird allem Leid bald ein Ende machen,Gott wird allem Leid bald ein Ende machen,"Offb. 21:3, 4","Offb. 21:3, 4","Siehe, das Zelt Gottes ist bei den Menschen. Er wird jede Träne von ihren Augen abwischen, und der Tod wird nicht mehr sein, noch Trauer, noch Schrei, noch Schmerz; die früheren Dinge sind vergangen.",Siehe;das;Zelt;Gottes
|
||||||
|
Tod,Die Toten sind ohne jedes Bewusstsein,Die Toten sind ohne jedes Bewusstsein,Pred. 9:5,Pred. 9:5,"Die Lebenden wissen, dass sie sterben; aber die Toten wissen gar nichts.",Die;Lebenden;wissen;dass
|
||||||
|
Tod,Die Toten sind ohne jedes Bewusstsein,Die Toten sind ohne jedes Bewusstsein,Ps. 115:17,Ps. 115:17,"Die Toten loben Jah nicht, keiner von denen, die ins Schweigen hinabfahren.",Die;Toten;loben;Jah
|
||||||
|
Tod,Die Toten können uns weder helfen noch uns schaden,Die Toten können uns weder helfen noch uns schaden,Ps. 146:4,Ps. 146:4,"Sein Geist verlässt ihn, er kehrt zur Erde zurück; an dem Tag vergehen seine Gedanken.",Sein;Geist;verlässt;ihn
|
||||||
|
Tod,Die Toten können uns weder helfen noch uns schaden,Die Toten können uns weder helfen noch uns schaden,5. Mo. 18:10–11,5. Mo. 18:10–11,"Bei dir soll niemand gefunden werden, der seinen Sohn oder seine Tochter im Feuer opfert, der Wahrsagerei treibt, Zauberei betreibt, nach Omen Ausschau hält, ein Zauberer ist oder einen Geisterbeschwörer befragt oder Tote befragt.",Bei;dir;soll;niemand
|
||||||
|
Tod,Unsere geliebten Verstorbenen werden auferweckt,Unsere geliebten Verstorbenen werden auferweckt,Hiob 14:13–15,Hiob 14:13–15,"O dass du mich im Grab verbergen würdest ... Du wirst rufen, und ich werde dir antworten; du wirst dich nach dem Werk deiner Hände sehnen.",O;dass;du;mich
|
||||||
|
Tod,Unsere geliebten Verstorbenen werden auferweckt,Unsere geliebten Verstorbenen werden auferweckt,"Joh. 5:28, 29","Joh. 5:28, 29","Wundert euch nicht darüber, denn die Stunde kommt, in der alle, die in den Gräbern sind, seine Stimme hören und hervorkommen werden: Die Gutes getan haben, zur Auferstehung des Lebens, die Schlechtes getan haben, zur Auferstehung des Gerichts.",Wundert;euch;nicht;darüber
|
||||||
|
Tod,Den Tod wird es nicht mehr geben,Den Tod wird es nicht mehr geben,"Offb. 21:3, 4","Offb. 21:3, 4","Siehe, das Zelt Gottes ist bei den Menschen. Er wird jede Träne von ihren Augen abwischen, und der Tod wird nicht mehr sein, noch Trauer, noch Schrei, noch Schmerz; die früheren Dinge sind vergangen.",Siehe;das;Zelt;Gottes
|
||||||
|
Tod,Den Tod wird es nicht mehr geben,Den Tod wird es nicht mehr geben,1. Kor. 15:26,1. Kor. 15:26,"Der letzte Feind, der vernichtet wird, ist der Tod.",Der;letzte;Feind;der
|
||||||
|
Religion,Gott ist nicht mit allen Religionen einverstanden,Gott ist nicht mit allen Religionen einverstanden,Jer. 7:11,Jer. 7:11,"Ist dieses Haus, das nach meinem Namen genannt ist, in euren Augen zu einer Räuberhöhle geworden?",Ist;dieses;Haus;das
|
||||||
|
Religion,Gott ist nicht mit allen Religionen einverstanden,Gott ist nicht mit allen Religionen einverstanden,Mat. 15:9,Mat. 15:9,"Vergeblich verehren sie mich, denn sie lehren als Lehren Menschengebote.",Vergeblich;verehren;sie;mich
|
||||||
|
Religion,Gott hasst Heuchelei,Gott hasst Heuchelei,Jes. 29:13,Jes. 29:13,"Dieses Volk naht sich mir mit seinem Mund und ehrt mich mit seinen Lippen, aber ihr Herz ist weit von mir entfernt.",Dieses;Volk;naht;sich
|
||||||
|
Religion,Gott hasst Heuchelei,Gott hasst Heuchelei,Mat. 23:27–28,Mat. 23:27–28,"Wehe euch, ihr Schriftgelehrten und Pharisäer, ihr Heuchler! Ihr seid wie getünchte Gräber, die außen schön aussehen, innen aber voller toter Menschen und aller Unreinheit sind. So erscheint ihr den Menschen gerecht, seid aber innen voller Heuchelei und Gesetzlosigkeit.",Wehe;euch;ihr;Schriftgelehrten
|
||||||
|
Religion,Die wahre Religion zeichnet sich durch echte Liebe aus,Die wahre Religion zeichnet sich durch echte Liebe aus,"Joh. 13:34, 35","Joh. 13:34, 35","Ein neues Gebot gebe ich euch, dass ihr einander liebt; so wie ich euch geliebt habe, sollt auch ihr einander lieben. Daran werden alle erkennen, dass ihr meine Jünger seid, wenn ihr Liebe zueinander habt.",Ein;neues;Gebot;gebe
|
||||||
|
Religion,Die wahre Religion zeichnet sich durch echte Liebe aus,Die wahre Religion zeichnet sich durch echte Liebe aus,1. Joh. 4:7–8,1. Joh. 4:7–8,"Geliebte, lasst uns einander lieben, denn die Liebe kommt von Gott. Jeder, der liebt, ist aus Gott geboren und erkennt Gott. Wer nicht liebt, hat Gott nicht erkannt, denn Gott ist Liebe.",Geliebte;lasst;uns;einander
|
||||||
|
1092
bibel-pwa/data.json
Normal file
BIN
bibel-pwa/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
bibel-pwa/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
bibel-pwa/icon.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
72
bibel-pwa/index.html
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
<title>Bibel Lern‑App</title>
|
||||||
|
<link rel="manifest" href="manifest.json" />
|
||||||
|
<link rel="icon" href="icon-192x192.png" />
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Splash Screen overlay -->
|
||||||
|
<div id="splash-screen" class="hidden">
|
||||||
|
<div class="splash-content">
|
||||||
|
<h1>Bibel Lern‑App</h1>
|
||||||
|
<p>Möchten Sie diese App installieren?</p>
|
||||||
|
<div class="splash-buttons">
|
||||||
|
<button id="install-btn" class="install">Installieren</button>
|
||||||
|
<button id="continue-btn" class="secondary">Weiter ohne Installation</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<header>
|
||||||
|
<div class="left-controls">
|
||||||
|
<select id="mode-select">
|
||||||
|
<option value="random">Zufall</option>
|
||||||
|
<option value="topic">Nach Thema</option>
|
||||||
|
<option value="book">Nach Bibelbuch</option>
|
||||||
|
</select>
|
||||||
|
<select id="topic-select" class="hidden"></select>
|
||||||
|
<select id="book-select" class="hidden"></select>
|
||||||
|
</div>
|
||||||
|
<div class="right-controls">
|
||||||
|
<span id="counter">0/0</span>
|
||||||
|
<label class="dark-toggle">
|
||||||
|
<input type="checkbox" id="dark-mode-toggle" />
|
||||||
|
<span>Dark</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="hints">
|
||||||
|
<label><input type="checkbox" id="comment-hint" /> Kommentar</label>
|
||||||
|
<label><input type="checkbox" id="keyword-hint" /> Schlüsselwörter</label>
|
||||||
|
<label><input type="checkbox" id="theme-hint" /> Thema</label>
|
||||||
|
</div>
|
||||||
|
<div id="card-container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-front">
|
||||||
|
<div class="card-symbol"></div>
|
||||||
|
<div class="card-ref"></div>
|
||||||
|
<div class="card-comment"></div>
|
||||||
|
<div class="card-keywords"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card-back">
|
||||||
|
<!-- Per‑Karten‑Zähler wird hier über dem Vers angezeigt -->
|
||||||
|
<div class="card-counter"></div>
|
||||||
|
<p class="card-text"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<button id="correct-btn" class="correct">Ich wusste den Text</button>
|
||||||
|
<button id="wrong-btn" class="wrong">Ich wusste ihn nicht</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
bibel-pwa/manifest.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "Bibel Lern‑App",
|
||||||
|
"short_name": "BibelLern",
|
||||||
|
"description": "Lerne Bibeltexte nach Themen und Büchern auswendig mit einem modernen Design.",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#f8f9fa",
|
||||||
|
"theme_color": "#007acc",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
638
bibel-pwa/script.js
Normal file
@@ -0,0 +1,638 @@
|
|||||||
|
/*
|
||||||
|
* Bibel Lern‑App
|
||||||
|
*
|
||||||
|
* Dieses Skript lädt die Daten aus einer separaten JSON‑Datei,
|
||||||
|
* gruppiert sie nach Themen, baut den Lernkarten‑Stapeln je nach Modus auf
|
||||||
|
* und verwaltet das Anzeigen von Karten, Zählern, Hinweisen und Dark‑Mode.
|
||||||
|
* Außerdem wird ein Splash‑Screen bereitgestellt, über den die App
|
||||||
|
* installiert werden kann, sobald das Browser‑Ereignis
|
||||||
|
* `beforeinstallprompt` ausgelöst wird.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
// Fallback‑Daten, falls data.json nicht geladen werden kann (z.B. beim file://‑Protokoll).
|
||||||
|
const fallbackData = [
|
||||||
|
{
|
||||||
|
"Thema": "Zukunft",
|
||||||
|
"Kommentar": "Die aktuelle Weltlage und die Einstellung der Menschen deuten darauf hin, dass große Veränderungen bevorstehen",
|
||||||
|
"Bibel-Verweis": "Mat. 24:3",
|
||||||
|
"Bibeltext": "Und als er auf dem Ölberg saß, traten zu ihm seine Jünger allein und fragten: Sage uns, wann wird das geschehen, und was wird das Zeichen deines Kommens und des Endes der Welt?",
|
||||||
|
"Schluesselwoerter": ["Veränderungen", "Zeichen"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Zukunft",
|
||||||
|
"Kommentar": "Die Erde wird nie zerstört werden",
|
||||||
|
"Bibel-Verweis": "Ps. 104:5",
|
||||||
|
"Bibeltext": "Der du die Erde auf ihre Grundfesten gegründet hast, daß sie für immer und ewig steht.",
|
||||||
|
"Schluesselwoerter": ["Erde", "ewig"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Zukunft",
|
||||||
|
"Kommentar": "Die Umweltzerstörung wird vollständig rückgängig gemacht",
|
||||||
|
"Bibel-Verweis": "Jes. 35:1-2",
|
||||||
|
"Bibeltext": "Die Wüste und Einöde wird lustig sein, und das dürre Land wird frohlocken; sie wird blühen wie die Lilien.",
|
||||||
|
"Schluesselwoerter": ["Wüste", "blühen"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Zukunft",
|
||||||
|
"Kommentar": "Alle Menschen werden völlig gesund sein",
|
||||||
|
"Bibel-Verweis": "Jes. 33:24",
|
||||||
|
"Bibeltext": "Kein Einwohner wird sagen: Ich bin krank; dem Volk, das dort wohnt, wird seine Schuld vergeben sein.",
|
||||||
|
"Schluesselwoerter": ["gesund", "heil"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Zukunft",
|
||||||
|
"Kommentar": "Sie können für immer auf der Erde leben",
|
||||||
|
"Bibel-Verweis": "Ps. 37:29",
|
||||||
|
"Bibeltext": "Die Gerechten erben das Land und bleiben ewiglich darin.",
|
||||||
|
"Schluesselwoerter": ["erben", "Land"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Familie",
|
||||||
|
"Kommentar": "Ein Ehemann sollte seine Frau „lieben wie sich selbst“",
|
||||||
|
"Bibel-Verweis": "Eph. 5:33",
|
||||||
|
"Bibeltext": "Doch auch ihr, jeder von euch liebe seine Frau wie sich selbst; die Frau aber erweise dem Mann Ehrfurcht.",
|
||||||
|
"Schluesselwoerter": ["Liebe", "Respekt"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Familie",
|
||||||
|
"Kommentar": "Eine Ehefrau sollte ihren Mann von Herzen respektieren",
|
||||||
|
"Bibel-Verweis": "Eph. 5:33",
|
||||||
|
"Bibeltext": "Doch auch ihr, jeder von euch liebe seine Frau wie sich selbst; die Frau aber erweise dem Mann Ehrfurcht.",
|
||||||
|
"Schluesselwoerter": ["Respekt", "Ehemann"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Familie",
|
||||||
|
"Kommentar": "Mann und Frau sollten einander treu sein",
|
||||||
|
"Bibel-Verweis": "Mal. 2:16",
|
||||||
|
"Bibeltext": "Denn ich hasse die Scheidung, spricht der HERR … so hütet euch in eurem Geist und seid nicht treulos.",
|
||||||
|
"Schluesselwoerter": ["Treue", "Ehe"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Familie",
|
||||||
|
"Kommentar": "Es wirkt sich auf Kinder gut aus, wenn sie ihre Eltern respektieren und auf sie hören",
|
||||||
|
"Bibel-Verweis": "Spr. 1:8-9",
|
||||||
|
"Bibeltext": "Höre, mein Sohn, die Unterweisung deines Vaters und verlaß nicht das Gesetz deiner Mutter; denn das wird deinem Haupt ein lieblicher Schmuck sein.",
|
||||||
|
"Schluesselwoerter": ["Kinder", "Gehorsam"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Gott",
|
||||||
|
"Kommentar": "Gott hat einen Namen",
|
||||||
|
"Bibel-Verweis": "Ps. 83:18",
|
||||||
|
"Bibeltext": "… damit man erkenne: Du allein, dessen Name Jehova ist, der Höchste bist über die ganze Erde.",
|
||||||
|
"Schluesselwoerter": ["Name", "Jehova"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Gott",
|
||||||
|
"Kommentar": "Gott kommuniziert mit uns",
|
||||||
|
"Bibel-Verweis": "2. Tim. 3:16-17",
|
||||||
|
"Bibeltext": "Alle Schrift ist von Gott eingegeben und nütze zur Lehre, zur Zurechtweisung, zur Besserung … damit der Mensch Gottes vollkommen sei.",
|
||||||
|
"Schluesselwoerter": ["Schrift", "göttlich"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Gott",
|
||||||
|
"Kommentar": "Gott ist fair und hat keine Vorurteile",
|
||||||
|
"Bibel-Verweis": "5. Mo. 10:17",
|
||||||
|
"Bibeltext": "Denn der HERR, euer Gott, … ist kein Anseher der Person und nimmt keine Bestechung an.",
|
||||||
|
"Schluesselwoerter": ["gerecht", "unparteiisch"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Gott",
|
||||||
|
"Kommentar": "Gott möchte uns helfen",
|
||||||
|
"Bibel-Verweis": "Ps. 46:1",
|
||||||
|
"Bibeltext": "Gott ist unsre Zuflucht und Stärke, ein bewährter Helfer in Nöten.",
|
||||||
|
"Schluesselwoerter": ["Hilfe", "Zuflucht"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Gebet",
|
||||||
|
"Kommentar": "Gott wünscht sich, dass wir zu ihm beten",
|
||||||
|
"Bibel-Verweis": "Ps. 62:8",
|
||||||
|
"Bibeltext": "Vertraut auf ihn allezeit, liebe Leute, schüttet euer Herz vor ihm aus; Gott ist unsere Zuflucht.",
|
||||||
|
"Schluesselwoerter": ["Beten", "vertrauen"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Gebet",
|
||||||
|
"Kommentar": "Aus der Bibel erfahren wir, wie man beten sollte",
|
||||||
|
"Bibel-Verweis": "Mat. 6:7-13",
|
||||||
|
"Bibeltext": "Wenn ihr betet, sollt ihr nicht plappern … Unser Vater im Himmel! Dein Name werde geheiligt …",
|
||||||
|
"Schluesselwoerter": ["Gebet", "Vaterunser"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Gebet",
|
||||||
|
"Kommentar": "Wir sollten oft beten",
|
||||||
|
"Bibel-Verweis": "Mat. 7:7-8",
|
||||||
|
"Bibeltext": "Bittet, so wird euch gegeben; suchet, so werdet ihr finden; klopfet an, so wird euch aufgetan.",
|
||||||
|
"Schluesselwoerter": ["bitten", "suchen"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Jesus",
|
||||||
|
"Kommentar": "Jesus war ein außergewöhnlicher Lehrer. Sein Rat funktioniert immer",
|
||||||
|
"Bibel-Verweis": "Mat. 6:14-15",
|
||||||
|
"Bibeltext": "Denn wenn ihr den Menschen ihre Verfehlungen vergebt, wird euch euer himmlischer Vater auch vergeben.",
|
||||||
|
"Schluesselwoerter": ["Vergebung", "Lehrer"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Jesus",
|
||||||
|
"Kommentar": "Jesus hat Ereignisse vorausgesagt, die wir heute beobachten können",
|
||||||
|
"Bibel-Verweis": "Mat. 24:3",
|
||||||
|
"Bibeltext": "Und als er auf dem Ölberg saß, traten zu ihm seine Jünger allein und fragten: Sage uns, wann wird das geschehen, und was wird das Zeichen deines Kommens und des Endes der Welt?",
|
||||||
|
"Schluesselwoerter": ["Prophezeiung", "Endzeit"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Jesus",
|
||||||
|
"Kommentar": "Jesus ist Gottes Sohn",
|
||||||
|
"Bibel-Verweis": "Mat. 16:16",
|
||||||
|
"Bibeltext": "Du bist der Christus, der Sohn des lebendigen Gottes.",
|
||||||
|
"Schluesselwoerter": ["Christus", "Sohn"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Jesus",
|
||||||
|
"Kommentar": "Jesus ist nicht der allmächtige Gott",
|
||||||
|
"Bibel-Verweis": "Joh. 14:28",
|
||||||
|
"Bibeltext": "… denn der Vater ist größer als ich.",
|
||||||
|
"Schluesselwoerter": ["Vater", "größer"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Reich Gottes",
|
||||||
|
"Kommentar": "Das Reich Gottes ist eine echte Regierung im Himmel",
|
||||||
|
"Bibel-Verweis": "Dan. 2:44",
|
||||||
|
"Bibeltext": "Aber zur Zeit solcher Königreiche wird der Gott des Himmels ein Königreich aufrichten, das nimmermehr zerstört wird; und sein Königreich wird auf kein ander Volk kommen. Es wird alle diese Königreiche zermalmen und verstören; aber es selbst wird ewiglich bleiben.",
|
||||||
|
"Schluesselwoerter": ["Königreich", "Himmel"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Reich Gottes",
|
||||||
|
"Kommentar": "Gottes Reich wird alle Regierungen auf der Erde ersetzen",
|
||||||
|
"Bibel-Verweis": "Ps. 2:7-9",
|
||||||
|
"Bibeltext": "Du bist mein Sohn; heute habe ich dich gezeugt … Zerbrich sie mit eisernem Zepter und zerschmettere sie.",
|
||||||
|
"Schluesselwoerter": ["Regierungen", "ersetzen"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Reich Gottes",
|
||||||
|
"Kommentar": "Nur Gottes Reich kann die Probleme der Menschheit lösen",
|
||||||
|
"Bibel-Verweis": "Ps. 37:10-11",
|
||||||
|
"Bibeltext": "Nur noch eine kleine Weile, so ist der Gottlose nicht mehr; … die Sanftmütigen aber werden das Land erben und großen Frieden haben.",
|
||||||
|
"Schluesselwoerter": ["Frieden", "Lösung"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Leid",
|
||||||
|
"Kommentar": "Gott ist nicht schuld, wenn wir leiden",
|
||||||
|
"Bibel-Verweis": "5. Mo. 32:4",
|
||||||
|
"Bibeltext": "Er ist ein Fels; vollkommen ist sein Tun, denn alle seine Wege sind recht; ein treuer Gott, ohne Falsch.",
|
||||||
|
"Schluesselwoerter": ["gerecht", "Schuld"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Leid",
|
||||||
|
"Kommentar": "Diese Welt wird vom Teufel beherrscht",
|
||||||
|
"Bibel-Verweis": "Luk. 4:5-6",
|
||||||
|
"Bibeltext": "Da führte ihn der Teufel auf einen hohen Berg … Dir will ich alle Macht und Herrlichkeit dieser Reiche geben, denn sie ist mir übergeben.",
|
||||||
|
"Schluesselwoerter": ["Teufel", "Welt"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Leid",
|
||||||
|
"Kommentar": "Gott sieht es, wenn Sie leiden, und möchte Ihnen helfen",
|
||||||
|
"Bibel-Verweis": "Ps. 34:17-19",
|
||||||
|
"Bibeltext": "Der HERR ist nahe denen, die zerbrochenen Herzens sind … Er erlöst sie aus allen ihren Nöten.",
|
||||||
|
"Schluesselwoerter": ["nah", "helfen"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Leid",
|
||||||
|
"Kommentar": "Gott wird allem Leid bald ein Ende machen",
|
||||||
|
"Bibel-Verweis": "Jes. 65:17",
|
||||||
|
"Bibeltext": "Denn siehe, ich schaffe einen neuen Himmel und eine neue Erde …",
|
||||||
|
"Schluesselwoerter": ["neuer Himmel", "Ende"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Tod",
|
||||||
|
"Kommentar": "Die Toten sind ohne jedes Bewusstsein und leiden auch nicht",
|
||||||
|
"Bibel-Verweis": "Pred. 9:5",
|
||||||
|
"Bibeltext": "Denn die Lebenden wissen, dass sie sterben; die Toten aber wissen nichts …",
|
||||||
|
"Schluesselwoerter": ["Toten", "Bewusstsein"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Tod",
|
||||||
|
"Kommentar": "Die Toten können uns weder helfen noch uns schaden",
|
||||||
|
"Bibel-Verweis": "Ps. 146:4",
|
||||||
|
"Bibeltext": "Wenn ein Mensch stirbt, kehrt sein Geist zurück zur Erde; an demselben Tag sind alle seine Pläne zunichte.",
|
||||||
|
"Schluesselwoerter": ["helfen", "schaden"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Tod",
|
||||||
|
"Kommentar": "Unsere geliebten Verstorbenen werden auferweckt",
|
||||||
|
"Bibel-Verweis": "Hiob 14:13-15",
|
||||||
|
"Bibeltext": "Wenn du mich nur im Totenreich verwahrtest … du würdest rufen, und ich würde dir antworten.",
|
||||||
|
"Schluesselwoerter": ["Auferweckung", "Hoffnung"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Tod",
|
||||||
|
"Kommentar": "Den Tod wird es nicht mehr geben",
|
||||||
|
"Bibel-Verweis": "Offb. 21:3-4",
|
||||||
|
"Bibeltext": "Gott wird abwischen alle Tränen von ihren Augen; der Tod wird nicht mehr sein …",
|
||||||
|
"Schluesselwoerter": ["Tod", "nicht mehr"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Religion",
|
||||||
|
"Kommentar": "Gott ist nicht mit allen Religionen einverstanden",
|
||||||
|
"Bibel-Verweis": "Jer. 7:11",
|
||||||
|
"Bibeltext": "Ist denn dieses Haus, das nach meinem Namen genannt ist, eine Räuberhöhle geworden in euren Augen?",
|
||||||
|
"Schluesselwoerter": ["Religion", "Räuberhöhle"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Religion",
|
||||||
|
"Kommentar": "Gott hasst Heuchelei",
|
||||||
|
"Bibel-Verweis": "Jes. 29:13",
|
||||||
|
"Bibeltext": "Dieses Volk naht mir mit seinem Mund … aber ihr Herz ist fern von mir.",
|
||||||
|
"Schluesselwoerter": ["Heuchelei", "Herz"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Thema": "Religion",
|
||||||
|
"Kommentar": "Die wahre Religion zeichnet sich durch echte Liebe aus",
|
||||||
|
"Bibel-Verweis": "Joh. 13:34-35",
|
||||||
|
"Bibeltext": "Ein neues Gebot gebe ich euch: dass ihr einander liebt … Daran werden alle erkennen, dass ihr meine Jünger seid.",
|
||||||
|
"Schluesselwoerter": ["Liebe", "Gebot"]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const modeSelect = document.getElementById('mode-select');
|
||||||
|
const topicSelect = document.getElementById('topic-select');
|
||||||
|
const bookSelect = document.getElementById('book-select');
|
||||||
|
const commentHint = document.getElementById('comment-hint');
|
||||||
|
const keywordHint = document.getElementById('keyword-hint');
|
||||||
|
const themeHint = document.getElementById('theme-hint');
|
||||||
|
const cardEl = document.querySelector('.card');
|
||||||
|
const cardFrontSymbol = document.querySelector('.card-symbol');
|
||||||
|
const cardFrontRef = document.querySelector('.card-ref');
|
||||||
|
const cardFrontComment = document.querySelector('.card-comment');
|
||||||
|
const cardBackText = document.querySelector('.card-back .card-text');
|
||||||
|
const cardFrontKeywords = document.querySelector('.card-front .card-keywords');
|
||||||
|
const cardBackCounter = document.querySelector('.card-back .card-counter');
|
||||||
|
const correctBtn = document.getElementById('correct-btn');
|
||||||
|
const wrongBtn = document.getElementById('wrong-btn');
|
||||||
|
const counterDisplay = document.getElementById('counter');
|
||||||
|
const darkToggle = document.getElementById('dark-mode-toggle');
|
||||||
|
const splashScreen = document.getElementById('splash-screen');
|
||||||
|
const installBtn = document.getElementById('install-btn');
|
||||||
|
const continueBtn = document.getElementById('continue-btn');
|
||||||
|
|
||||||
|
// Mapping von Themen zu Symbolen und Farben
|
||||||
|
const topicMapping = {
|
||||||
|
'Zukunft': { color: '#B2E0F1', symbol: '⏳' },
|
||||||
|
'Familie': { color: '#FFEFA1', symbol: '👨👩👧👦' },
|
||||||
|
'Gott': { color: '#C3EACE', symbol: '🕊️' },
|
||||||
|
'Gebet': { color: '#F9B5C8', symbol: '🙏' },
|
||||||
|
'Jesus': { color: '#FFD0B5', symbol: '✝️' },
|
||||||
|
'Reich Gottes': { color: '#ADD7F6', symbol: '👑' },
|
||||||
|
'Leid': { color: '#D3C0E6', symbol: '😢' },
|
||||||
|
'Tod': { color: '#B7D4E7', symbol: '⚰️' },
|
||||||
|
'Religion': { color: '#FFC8BF', symbol: '⛪️' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Daten werden nach dem Laden der Seite geladen
|
||||||
|
let data = [];
|
||||||
|
let topics = {};
|
||||||
|
let uniqueBooks = [];
|
||||||
|
let deck = [];
|
||||||
|
let currentIndex = 0;
|
||||||
|
// Zähler für aktuelle Session
|
||||||
|
let correctCount = 0;
|
||||||
|
// Fortlaufende Statistik über alle Sessions hinweg nach Thema
|
||||||
|
let progressData = {};
|
||||||
|
// Statistik pro Karte (richtig/falsch)
|
||||||
|
let cardStats = {};
|
||||||
|
|
||||||
|
let deferredPrompt = null;
|
||||||
|
|
||||||
|
// Gruppiert Daten nach Thema und sammelt Buchlisten
|
||||||
|
function groupData() {
|
||||||
|
topics = {};
|
||||||
|
const booksSet = new Set();
|
||||||
|
data.forEach(item => {
|
||||||
|
const theme = item['Thema'];
|
||||||
|
if (!topics[theme]) {
|
||||||
|
topics[theme] = {
|
||||||
|
color: (topicMapping[theme] && topicMapping[theme].color) || '#CCCCCC',
|
||||||
|
symbol: (topicMapping[theme] && topicMapping[theme].symbol) || '📖',
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
topics[theme].items.push(item);
|
||||||
|
// Extrahiere Buch zur Liste
|
||||||
|
const book = extractBook(item['Bibel-Verweis']);
|
||||||
|
booksSet.add(book);
|
||||||
|
});
|
||||||
|
uniqueBooks = Array.from(booksSet).sort((a, b) => a.localeCompare(b, 'de')); // sortiert nach deutschem Collation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrahiert das Buch aus einem Verweis (z.B. "2. Tim. 3:1-5" -> "2. Tim.")
|
||||||
|
function extractBook(ref) {
|
||||||
|
const beforeColon = ref.split(':')[0].trim();
|
||||||
|
const parts = beforeColon.split(' ');
|
||||||
|
// Entferne die letzte Komponente (Kapitel)
|
||||||
|
if (parts.length > 1) parts.pop();
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generiert einen eindeutigen Schlüssel für eine Karte. Wir verwenden Thema,
|
||||||
|
// den Bibel‑Verweis und den Kommentar, um Karten eindeutig zu identifizieren.
|
||||||
|
function getCardKey(item) {
|
||||||
|
const theme = item['Thema'] || '';
|
||||||
|
const ref = item['Bibel-Verweis'] || '';
|
||||||
|
const comment = item['Kommentar'] || '';
|
||||||
|
return `${theme}|${ref}|${comment}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Füllt die Auswahlfelder für Thema und Buch
|
||||||
|
function populateSelects() {
|
||||||
|
// Themenauswahl
|
||||||
|
topicSelect.innerHTML = '';
|
||||||
|
Object.keys(topics).forEach(name => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = name;
|
||||||
|
option.textContent = name;
|
||||||
|
topicSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
// Buchauswahl
|
||||||
|
bookSelect.innerHTML = '';
|
||||||
|
uniqueBooks.forEach(name => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = name;
|
||||||
|
option.textContent = name;
|
||||||
|
bookSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mischt ein Array (Fisher–Yates)
|
||||||
|
function shuffle(array) {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bereitet das Karten‑Deck entsprechend dem aktuellen Modus vor
|
||||||
|
function prepareDeck() {
|
||||||
|
const mode = modeSelect.value;
|
||||||
|
let items = [];
|
||||||
|
if (mode === 'random') {
|
||||||
|
// Alle Elemente
|
||||||
|
items = [...data];
|
||||||
|
} else if (mode === 'topic') {
|
||||||
|
const selectedTheme = topicSelect.value;
|
||||||
|
items = topics[selectedTheme] ? [...topics[selectedTheme].items] : [];
|
||||||
|
} else if (mode === 'book') {
|
||||||
|
const selectedBook = bookSelect.value;
|
||||||
|
items = data.filter(item => extractBook(item['Bibel-Verweis']) === selectedBook);
|
||||||
|
}
|
||||||
|
deck = shuffle(items);
|
||||||
|
currentIndex = 0;
|
||||||
|
correctCount = 0;
|
||||||
|
updateCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiert die Anzeige der aktuellen Karte
|
||||||
|
function updateCard() {
|
||||||
|
if (deck.length === 0) {
|
||||||
|
cardFrontRef.textContent = 'Keine Karten';
|
||||||
|
cardBackText.textContent = '';
|
||||||
|
cardFrontSymbol.textContent = '';
|
||||||
|
cardEl.classList.remove('flipped');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const item = deck[currentIndex];
|
||||||
|
const theme = item['Thema'];
|
||||||
|
const mapping = topicMapping[theme] || { color: '#CCCCCC', symbol: '📖' };
|
||||||
|
// Setze die Akzentfarbe über CSS‑Variable
|
||||||
|
cardFrontRef.parentElement.style.setProperty('--accent-color', mapping.color);
|
||||||
|
cardBackText.parentElement.style.setProperty('--accent-color', mapping.color);
|
||||||
|
// Thema als Text oder leer je nach Hint
|
||||||
|
if (themeHint && themeHint.checked) {
|
||||||
|
cardFrontSymbol.textContent = theme;
|
||||||
|
} else {
|
||||||
|
cardFrontSymbol.textContent = '';
|
||||||
|
}
|
||||||
|
// Referenz immer ohne Keywords anzeigen
|
||||||
|
cardFrontRef.textContent = item['Bibel-Verweis'];
|
||||||
|
// Kommentar sichtbar machen, wenn aktiviert
|
||||||
|
if (commentHint.checked) {
|
||||||
|
cardFrontComment.textContent = item['Kommentar'];
|
||||||
|
} else {
|
||||||
|
cardFrontComment.textContent = '';
|
||||||
|
}
|
||||||
|
// Setze Text der Rückseite
|
||||||
|
cardBackText.textContent = item['Bibeltext'];
|
||||||
|
// Aktualisiere die Per‑Karten‑Zähleranzeige
|
||||||
|
if (cardBackCounter) {
|
||||||
|
const key = getCardKey(item);
|
||||||
|
const stats = cardStats[key] || { correct: 0, wrong: 0 };
|
||||||
|
const correctVal = stats.correct || 0;
|
||||||
|
const wrongVal = stats.wrong || 0;
|
||||||
|
cardBackCounter.textContent = `Richtig: ${correctVal} | Falsch: ${wrongVal}`;
|
||||||
|
}
|
||||||
|
// Schlüsselwörter auf der Vorderseite, falls aktiviert
|
||||||
|
if (keywordHint && keywordHint.checked) {
|
||||||
|
const keywords = item['Schluesselwoerter'] || [];
|
||||||
|
cardFrontKeywords.textContent = keywords.join(', ');
|
||||||
|
} else {
|
||||||
|
cardFrontKeywords.textContent = '';
|
||||||
|
}
|
||||||
|
// Stellen Sie sicher, dass die Karte initial nicht umgedreht ist
|
||||||
|
cardEl.classList.remove('flipped');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiert den Counter oben rechts
|
||||||
|
function updateCounter() {
|
||||||
|
const total = deck.length;
|
||||||
|
counterDisplay.textContent = `${correctCount}/${total}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wechselt zur nächsten Karte im Stapel
|
||||||
|
function nextCard() {
|
||||||
|
if (deck.length === 0) return;
|
||||||
|
currentIndex = (currentIndex + 1) % deck.length;
|
||||||
|
updateCard();
|
||||||
|
updateCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ereignis‑Listener für Dark‑Mode
|
||||||
|
function handleDarkModeChange() {
|
||||||
|
const dark = darkToggle.checked;
|
||||||
|
document.body.classList.toggle('dark', dark);
|
||||||
|
localStorage.setItem('darkMode', dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisiert die Splash‑Screen‑Logik
|
||||||
|
function initSplash() {
|
||||||
|
// Zeige Splash nur, wenn noch nicht gesehen
|
||||||
|
const dismissed = localStorage.getItem('splashDismissed');
|
||||||
|
if (dismissed) return;
|
||||||
|
splashScreen.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registriert Service Worker
|
||||||
|
function registerSW() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register('service-worker.js')
|
||||||
|
.catch(err => console.error('Service Worker registration failed:', err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Haupt‑Initialisierung
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
// Lade Daten
|
||||||
|
try {
|
||||||
|
if (location.protocol === 'file:') {
|
||||||
|
// Beim direkten Öffnen über das Dateisystem schlägt fetch typischerweise fehl.
|
||||||
|
// Verwenden Sie in diesem Fall Fallback‑Daten aus dieser Datei.
|
||||||
|
data = fallbackData;
|
||||||
|
} else {
|
||||||
|
const res = await fetch('data.json');
|
||||||
|
if (!res.ok) throw new Error('Network response was not ok');
|
||||||
|
data = await res.json();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Fehler beim Laden der Daten, verwende Fallback‑Daten:', err);
|
||||||
|
data = fallbackData;
|
||||||
|
}
|
||||||
|
// Lade gespeicherte Zähler aus localStorage
|
||||||
|
try {
|
||||||
|
const storedProgress = localStorage.getItem('progressData');
|
||||||
|
if (storedProgress) progressData = JSON.parse(storedProgress);
|
||||||
|
} catch (e) {
|
||||||
|
progressData = {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const storedStats = localStorage.getItem('cardStats');
|
||||||
|
if (storedStats) cardStats = JSON.parse(storedStats);
|
||||||
|
} catch (e) {
|
||||||
|
cardStats = {};
|
||||||
|
}
|
||||||
|
// Gruppiere Daten und fülle Auswahlfelder
|
||||||
|
groupData();
|
||||||
|
populateSelects();
|
||||||
|
// Auswahllisten je nach Modus initial ausblenden
|
||||||
|
topicSelect.classList.toggle('hidden', modeSelect.value !== 'topic');
|
||||||
|
bookSelect.classList.toggle('hidden', modeSelect.value !== 'book');
|
||||||
|
prepareDeck();
|
||||||
|
updateCard();
|
||||||
|
// Dark mode initial
|
||||||
|
const darkPref = localStorage.getItem('darkMode') === 'true';
|
||||||
|
darkToggle.checked = darkPref;
|
||||||
|
document.body.classList.toggle('dark', darkPref);
|
||||||
|
// Splash init
|
||||||
|
initSplash();
|
||||||
|
// Service Worker
|
||||||
|
registerSW();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event‑Listener für Mode‑Änderungen
|
||||||
|
modeSelect.addEventListener('change', () => {
|
||||||
|
const mode = modeSelect.value;
|
||||||
|
topicSelect.classList.toggle('hidden', mode !== 'topic');
|
||||||
|
bookSelect.classList.toggle('hidden', mode !== 'book');
|
||||||
|
prepareDeck();
|
||||||
|
updateCard();
|
||||||
|
});
|
||||||
|
|
||||||
|
topicSelect.addEventListener('change', () => {
|
||||||
|
prepareDeck();
|
||||||
|
updateCard();
|
||||||
|
});
|
||||||
|
bookSelect.addEventListener('change', () => {
|
||||||
|
prepareDeck();
|
||||||
|
updateCard();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hint toggles
|
||||||
|
commentHint.addEventListener('change', () => {
|
||||||
|
updateCard();
|
||||||
|
});
|
||||||
|
keywordHint.addEventListener('change', () => {
|
||||||
|
updateCard();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (themeHint) {
|
||||||
|
themeHint.addEventListener('change', () => {
|
||||||
|
updateCard();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card click – flip
|
||||||
|
cardEl.addEventListener('click', () => {
|
||||||
|
if (deck.length === 0) return;
|
||||||
|
cardEl.classList.toggle('flipped');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Korrekt/Wrong Buttons
|
||||||
|
correctBtn.addEventListener('click', () => {
|
||||||
|
if (deck.length === 0) return;
|
||||||
|
// Aktuelle Karte
|
||||||
|
const item = deck[currentIndex];
|
||||||
|
const theme = item['Thema'];
|
||||||
|
const key = getCardKey(item);
|
||||||
|
// Session‑Counter erhöhen
|
||||||
|
correctCount++;
|
||||||
|
// Fortschrittsdaten pro Thema aktualisieren
|
||||||
|
if (!progressData[theme]) progressData[theme] = { correct: 0, wrong: 0 };
|
||||||
|
progressData[theme].correct++;
|
||||||
|
// Per‑Karten‑Statistik aktualisieren
|
||||||
|
if (!cardStats[key]) cardStats[key] = { correct: 0, wrong: 0 };
|
||||||
|
cardStats[key].correct++;
|
||||||
|
// Speichern in localStorage
|
||||||
|
try {
|
||||||
|
localStorage.setItem('progressData', JSON.stringify(progressData));
|
||||||
|
localStorage.setItem('cardStats', JSON.stringify(cardStats));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Speichern der Zählerdaten fehlgeschlagen:', e);
|
||||||
|
}
|
||||||
|
nextCard();
|
||||||
|
});
|
||||||
|
wrongBtn.addEventListener('click', () => {
|
||||||
|
if (deck.length === 0) return;
|
||||||
|
// Aktuelle Karte
|
||||||
|
const item = deck[currentIndex];
|
||||||
|
const theme = item['Thema'];
|
||||||
|
const key = getCardKey(item);
|
||||||
|
// Fortschrittsdaten pro Thema aktualisieren
|
||||||
|
if (!progressData[theme]) progressData[theme] = { correct: 0, wrong: 0 };
|
||||||
|
progressData[theme].wrong++;
|
||||||
|
// Per‑Karten‑Statistik aktualisieren
|
||||||
|
if (!cardStats[key]) cardStats[key] = { correct: 0, wrong: 0 };
|
||||||
|
cardStats[key].wrong++;
|
||||||
|
// Speichern in localStorage
|
||||||
|
try {
|
||||||
|
localStorage.setItem('progressData', JSON.stringify(progressData));
|
||||||
|
localStorage.setItem('cardStats', JSON.stringify(cardStats));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Speichern der Zählerdaten fehlgeschlagen:', e);
|
||||||
|
}
|
||||||
|
nextCard();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dark mode toggle
|
||||||
|
darkToggle.addEventListener('change', handleDarkModeChange);
|
||||||
|
|
||||||
|
// Splash‑Screen Buttons
|
||||||
|
continueBtn.addEventListener('click', () => {
|
||||||
|
splashScreen.classList.add('hidden');
|
||||||
|
localStorage.setItem('splashDismissed', 'true');
|
||||||
|
});
|
||||||
|
installBtn.addEventListener('click', async () => {
|
||||||
|
if (deferredPrompt) {
|
||||||
|
deferredPrompt.prompt();
|
||||||
|
const result = await deferredPrompt.userChoice;
|
||||||
|
console.log('Install result:', result);
|
||||||
|
deferredPrompt = null;
|
||||||
|
}
|
||||||
|
splashScreen.classList.add('hidden');
|
||||||
|
localStorage.setItem('splashDismissed', 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
// beforeinstallprompt event
|
||||||
|
window.addEventListener('beforeinstallprompt', e => {
|
||||||
|
// Verhindere die Standard‑Banneranzeige
|
||||||
|
e.preventDefault();
|
||||||
|
deferredPrompt = e;
|
||||||
|
// Falls der Splash noch sichtbar ist, zeige Install‑Button
|
||||||
|
installBtn.style.display = 'inline-block';
|
||||||
|
});
|
||||||
|
})();
|
||||||
68
bibel-pwa/service-worker.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Service Worker für die Bibel Lern‑App
|
||||||
|
* Dieser Worker sorgt dafür, dass die wesentlichen Dateien
|
||||||
|
* für den Offline‑Betrieb zwischengespeichert werden und
|
||||||
|
* veraltetete Caches bereinigt werden.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CACHE_NAME = 'bibel-app-cache-v3';
|
||||||
|
// Arbeite mit relativen Pfaden, damit die PWA auch aus einem Unterordner funktioniert.
|
||||||
|
const BASE_PATH = new URL(self.registration.scope).pathname.replace(/\/$/, '');
|
||||||
|
const scopePath = (path) => `${BASE_PATH}/${path}`;
|
||||||
|
const OFFLINE_URLS = [
|
||||||
|
'index.html',
|
||||||
|
'styles.css',
|
||||||
|
'script.js',
|
||||||
|
'manifest.json',
|
||||||
|
'data.json',
|
||||||
|
'data.csv',
|
||||||
|
'icon-192x192.png',
|
||||||
|
'icon-512x512.png'
|
||||||
|
].map(scopePath);
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
return cache.addAll(OFFLINE_URLS);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map((cacheName) => {
|
||||||
|
if (cacheName !== CACHE_NAME) {
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
if (event.request.method !== 'GET') return;
|
||||||
|
const requestUrl = new URL(event.request.url);
|
||||||
|
|
||||||
|
// Nur Anfragen innerhalb des eigenen Ursprungs beantworten, damit Offsite‑Requests
|
||||||
|
// nicht ungewollt überschrieben werden.
|
||||||
|
if (requestUrl.origin !== self.location.origin) return;
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((cached) => {
|
||||||
|
if (cached) return cached;
|
||||||
|
return fetch(event.request).catch(() => {
|
||||||
|
// Fallback bei HTML‑Anfragen
|
||||||
|
if (
|
||||||
|
event.request.mode === 'navigate' ||
|
||||||
|
event.request.headers.get('accept')?.includes('text/html')
|
||||||
|
) {
|
||||||
|
return caches.match(scopePath('index.html'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
326
bibel-pwa/styles.css
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
/* Grundlegende Styles für die Bibel‑Lern‑App */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Light mode Farben */
|
||||||
|
--background-color: #f8f9fa;
|
||||||
|
--header-bg: #ffffff;
|
||||||
|
--header-text: #333333;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--card-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
--text-color: #333333;
|
||||||
|
--button-bg: #007acc;
|
||||||
|
--button-text: #ffffff;
|
||||||
|
--button-bg-secondary: #e0e0e0;
|
||||||
|
--button-text-secondary: #333333;
|
||||||
|
--accent-default: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark {
|
||||||
|
/* Dark mode Farben */
|
||||||
|
--background-color: #121212;
|
||||||
|
--header-bg: #1f1f1f;
|
||||||
|
--header-text: #e0e0e0;
|
||||||
|
--card-bg: #2a2a2a;
|
||||||
|
--card-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||||
|
--text-color: #f0f0f0;
|
||||||
|
--button-bg: #005f8f;
|
||||||
|
--button-text: #ffffff;
|
||||||
|
--button-bg-secondary: #3a3a3a;
|
||||||
|
--button-text-secondary: #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||||
|
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
background: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background: var(--header-bg);
|
||||||
|
color: var(--header-text);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
header select {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-toggle input {
|
||||||
|
margin-right: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#counter {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hints {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hints input {
|
||||||
|
margin-right: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#card-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 420px;
|
||||||
|
/* erhöht die Kartenhöhe um 25 % für mehr Platz bei langen Versen */
|
||||||
|
height: 325px;
|
||||||
|
perspective: 1000px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-inner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: transform 0.6s;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.flipped .card-inner {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-front,
|
||||||
|
.card-back {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
color: var(--text-color);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-front {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border-top: 6px solid var(--accent-color, var(--accent-default));
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-symbol {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
font-size: 3rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-ref {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kommentar unter dem Verweis */
|
||||||
|
.card-comment {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-back {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
padding: 1rem;
|
||||||
|
border-top: 6px solid var(--accent-color, var(--accent-default));
|
||||||
|
/* Positioniert den Text am oberen Rand und lässt längeren Text scrollen */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
text-align: center;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zähler pro Lernkarte auf der Rückseite */
|
||||||
|
.card-back .card-counter {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-back .card-text {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
/* Lässt den Textbereich flexibel und scrollbar werden */
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stil für die Schlüsselwörter auf der Vorderseite der Karte */
|
||||||
|
.card-front .card-keywords {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
width: 100%;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons button {
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons .correct {
|
||||||
|
background: var(--button-bg);
|
||||||
|
color: var(--button-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons .wrong {
|
||||||
|
background: var(--button-bg-secondary);
|
||||||
|
color: var(--button-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons .correct:hover {
|
||||||
|
background: #005fa3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons .wrong:hover {
|
||||||
|
background: #c7c7c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Splash Screen Styles */
|
||||||
|
#splash-screen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#splash-screen.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-content {
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
text-align: center;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-content h1 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-content p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-buttons button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.6rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-buttons .install {
|
||||||
|
background: var(--button-bg);
|
||||||
|
color: var(--button-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-buttons .secondary {
|
||||||
|
background: var(--button-bg-secondary);
|
||||||
|
color: var(--button-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dienstklasse zum Ausblenden */
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.card {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
.card-symbol {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
config/.htaccess
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Files "config.json">
|
||||||
|
Require all denied
|
||||||
|
</Files>
|
||||||
6
config/config.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"user": "Thomas",
|
||||||
|
"pass": "Alles4Mich!"
|
||||||
|
}
|
||||||
|
}
|
||||||
258
index.php
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
<?php
|
||||||
|
// index.php – Basis-Menü + Login mit flexibler Config (YAML/JSON/INI)
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// ===== Pfade / Basis =====
|
||||||
|
$basePath = __DIR__;
|
||||||
|
$baseUrl = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\');
|
||||||
|
|
||||||
|
// ===== Config-Lader =====
|
||||||
|
function find_config_path(string $basePath): ?string {
|
||||||
|
// Reihenfolge der Dateitypen: YAML, YML, JSON, INI
|
||||||
|
$candidates = [
|
||||||
|
$basePath . '/config/config.yaml',
|
||||||
|
$basePath . '/config/config.yml',
|
||||||
|
$basePath . '/config/config.json',
|
||||||
|
$basePath . '/config/config.ini',
|
||||||
|
];
|
||||||
|
foreach ($candidates as $c) {
|
||||||
|
if (@is_file($c) && @is_readable($c)) return $c;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_yaml_with_any(string $path): ?array {
|
||||||
|
// 1) ext-yaml?
|
||||||
|
if (function_exists('yaml_parse_file')) {
|
||||||
|
$cfg = @yaml_parse_file($path);
|
||||||
|
return is_array($cfg) ? $cfg : null;
|
||||||
|
}
|
||||||
|
// 2) Symfony/Yaml über Composer?
|
||||||
|
$autoload = __DIR__ . '/vendor/autoload.php';
|
||||||
|
if (is_file($autoload)) {
|
||||||
|
require_once $autoload;
|
||||||
|
if (class_exists(\Symfony\Component\Yaml\Yaml::class)) {
|
||||||
|
try {
|
||||||
|
$cfg = \Symfony\Component\Yaml\Yaml::parseFile($path);
|
||||||
|
return is_array($cfg) ? $cfg : null;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_config(string $basePath): array {
|
||||||
|
$path = find_config_path($basePath);
|
||||||
|
if (!$path) {
|
||||||
|
http_response_code(500);
|
||||||
|
die("Config-Datei nicht gefunden. Erwartet: config.yaml|yml|json|ini in " . htmlspecialchars($basePath . '/config'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||||
|
$config = null;
|
||||||
|
|
||||||
|
if (in_array($ext, ['yaml', 'yml'], true)) {
|
||||||
|
$config = parse_yaml_with_any($path);
|
||||||
|
if ($config === null) {
|
||||||
|
// Fallback: Versuche JSON/INI wenn vorhanden (z. B. User hat nur eine davon)
|
||||||
|
// Aber hier ist die Datei YAML – wenn nicht parsebar, liefern wir einen klaren Fehler.
|
||||||
|
http_response_code(500);
|
||||||
|
die("YAML-Parser nicht verfügbar oder Datei ungültig: " . htmlspecialchars(basename($path)) . ". Installiere 'ext-yaml' oder 'symfony/yaml', oder nutze JSON/INI.");
|
||||||
|
}
|
||||||
|
} elseif ($ext === 'json') {
|
||||||
|
$raw = @file_get_contents($path);
|
||||||
|
if ($raw === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
die("Config (JSON) konnte nicht gelesen werden: " . htmlspecialchars(basename($path)));
|
||||||
|
}
|
||||||
|
$config = json_decode($raw, true);
|
||||||
|
if (!is_array($config)) {
|
||||||
|
http_response_code(500);
|
||||||
|
die("Ungültige JSON-Config: " . htmlspecialchars(basename($path)));
|
||||||
|
}
|
||||||
|
} elseif ($ext === 'ini') {
|
||||||
|
$config = @parse_ini_file($path, true, INI_SCANNER_TYPED);
|
||||||
|
if (!is_array($config)) {
|
||||||
|
http_response_code(500);
|
||||||
|
die("Ungültige INI-Config: " . htmlspecialchars(basename($path)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
die("Nicht unterstützte Config-Erweiterung: " . htmlspecialchars($ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalisiere Struktur: wir erwarten auth.user / auth.pass
|
||||||
|
// Erlaube flach (user, pass) ODER verschachtelt (auth[user], auth[pass])
|
||||||
|
$user = $config['auth']['user'] ?? $config['user'] ?? null;
|
||||||
|
$pass = $config['auth']['pass'] ?? $config['pass'] ?? null;
|
||||||
|
|
||||||
|
if (!$user || !$pass) {
|
||||||
|
http_response_code(500);
|
||||||
|
die("Ungültige Config-Struktur. Erwartet z. B.:\n"
|
||||||
|
. "YAML/JSON:\n{\n \"auth\": {\"user\": \"Thomas\", \"pass\": \"Alles4Mich!\"}\n}\n"
|
||||||
|
. "INI:\n[auth]\nuser=Thomas\npass=Alles4Mich!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['user' => (string)$user, 'pass' => (string)$pass, 'path' => $path];
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = load_config($basePath);
|
||||||
|
$USERNAME = $cfg['user'];
|
||||||
|
$PASSWORD = $cfg['pass'];
|
||||||
|
|
||||||
|
// ===== Login / Logout Handling =====
|
||||||
|
if (isset($_GET['logout'])) {
|
||||||
|
$_SESSION = [];
|
||||||
|
if (ini_get("session.use_cookies")) {
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
setcookie(session_name(), '', time()-42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"]);
|
||||||
|
}
|
||||||
|
session_destroy();
|
||||||
|
header("Location: ".$baseUrl."/");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
|
||||||
|
$u = $_POST['user'] ?? '';
|
||||||
|
$p = $_POST['pass'] ?? '';
|
||||||
|
// hash_equals verhindert Timing-Angriffe
|
||||||
|
if (hash_equals($USERNAME, $u) && hash_equals($PASSWORD, $p)) {
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['auth'] = true;
|
||||||
|
header("Location: ".$baseUrl."/");
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$errors[] = "Login fehlgeschlagen.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$authed = ($_SESSION['auth'] ?? false) === true;
|
||||||
|
|
||||||
|
// ===== Verzeichnisse scannen =====
|
||||||
|
$apps = [];
|
||||||
|
if ($authed) {
|
||||||
|
foreach (glob($basePath . '/*', GLOB_ONLYDIR) as $dirPath) {
|
||||||
|
$name = basename($dirPath);
|
||||||
|
|
||||||
|
// Ordner, die NICHT im Menü erscheinen
|
||||||
|
if (in_array($name, ['vendor', '.git', '.well-known', 'config'], true)) continue;
|
||||||
|
|
||||||
|
$iconRel = $name . '/icon.png';
|
||||||
|
$iconAbs = $basePath . '/' . $iconRel;
|
||||||
|
$hasIcon = is_file($iconAbs);
|
||||||
|
|
||||||
|
$apps[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'url' => ($baseUrl ? $baseUrl : '') . '/' . rawurlencode($name) . '/',
|
||||||
|
'icon' => $hasIcon ? (($baseUrl ? $baseUrl : '') . '/' . $iconRel . '?v=' . filemtime($iconAbs)) : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
usort($apps, fn($a, $b) => strcasecmp($a['name'], $b['name']));
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>VIBING – Experimente</title>
|
||||||
|
<style>
|
||||||
|
:root { --bg:#0f1115; --card:#171a21; --fg:#e8eef9; --muted:#9aa4b2; --accent:#7aa2f7; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0; font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif;
|
||||||
|
background: radial-gradient(1200px 600px at 20% -10%, #182034, transparent), var(--bg); color: var(--fg);
|
||||||
|
min-height: 100svh; display: grid; place-items: center; padding: 24px;
|
||||||
|
}
|
||||||
|
.wrap { width: min(1100px, 100%); }
|
||||||
|
header { display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; }
|
||||||
|
header h1 { margin:0; font-size: clamp(20px, 3vw, 28px); letter-spacing:.3px; }
|
||||||
|
header a.logout{ color: var(--muted); text-decoration:none; border:1px solid #2a3140; padding:8px 12px; border-radius:10px; }
|
||||||
|
.card {
|
||||||
|
background: linear-gradient(180deg, #1a1f2b, var(--card));
|
||||||
|
border: 1px solid #242b3a; border-radius: 16px; padding: clamp(16px, 3vw, 24px); box-shadow: 0 10px 30px rgba(0,0,0,.25);
|
||||||
|
}
|
||||||
|
form.login { display:grid; gap:12px; max-width:340px; }
|
||||||
|
label { font-size:14px; color: var(--muted); }
|
||||||
|
input[type=text], input[type=password] {
|
||||||
|
width:100%; padding:12px 14px; border-radius:10px; border:1px solid #2a3140; background:#0f1320; color:var(--fg);
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding:12px 14px; border-radius:10px; border:1px solid #2e3650; background: #1f2740; color: var(--fg); cursor:pointer;
|
||||||
|
}
|
||||||
|
.error { color:#ff8b8b; margin:6px 0 0 0; font-size:14px; }
|
||||||
|
|
||||||
|
.grid { display:grid; gap:16px; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
|
||||||
|
.app { display:flex; flex-direction:column; gap:10px; padding:14px; border:1px solid #263147; border-radius:14px; background:#121726; text-decoration:none; color:inherit; }
|
||||||
|
.app:hover { border-color:#2f3c59; transform: translateY(-1px); transition: .15s ease; }
|
||||||
|
.icon {
|
||||||
|
width: 100%; aspect-ratio: 1/1; border-radius: 12px; background: #0d1220; display:grid; place-items:center;
|
||||||
|
overflow:hidden; border:1px solid #232b40;
|
||||||
|
}
|
||||||
|
.icon img { width:100%; height:100%; object-fit:cover; display:block; }
|
||||||
|
.fallback { font-size:38px; color: var(--accent); }
|
||||||
|
.name { font-weight:600; }
|
||||||
|
.empty { color: var(--muted); }
|
||||||
|
footer { margin-top:18px; color: var(--muted); font-size:13px; text-align:center; }
|
||||||
|
.hint { color: var(--muted); font-size: 12px; margin-top:8px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<header>
|
||||||
|
<h1>VIBING – Experimente</h1>
|
||||||
|
<?php if ($authed): ?>
|
||||||
|
<a class="logout" href="?logout=1">Logout</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<?php if (!$authed): ?>
|
||||||
|
<form class="login" method="post" autocomplete="off">
|
||||||
|
<div>
|
||||||
|
<label for="user">Benutzername</label>
|
||||||
|
<input id="user" name="user" type="text" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="pass">Passwort</label>
|
||||||
|
<input id="pass" name="pass" type="password" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button name="login" value="1" type="submit">Login</button>
|
||||||
|
<?php foreach ($errors as $e): ?>
|
||||||
|
<div class="error"><?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<div class="hint">
|
||||||
|
Unterstützte Config-Dateien: <code>config.yaml</code>, <code>config.yml</code>, <code>config.json</code>, <code>config.ini</code> im Ordner <code>config/</code>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php if (empty($apps)): ?>
|
||||||
|
<div class="empty">Noch keine Unterverzeichnisse gefunden. Lege Ordner auf Server-Ebene an (z. B. <code>/experiment1</code>) und optional ein <code>icon.png</code> hinein.</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="grid">
|
||||||
|
<?php foreach ($apps as $app): ?>
|
||||||
|
<a class="app" href="<?= htmlspecialchars($app['url'], ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
<div class="icon">
|
||||||
|
<?php if ($app['icon']): ?>
|
||||||
|
<img src="<?= htmlspecialchars($app['icon'], ENT_QUOTES, 'UTF-8') ?>" alt="icon">
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="fallback">🧪</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="name"><?= htmlspecialchars($app['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>Basis-Verzeichnis: <code><?= htmlspecialchars($baseUrl ?: '/', ENT_QUOTES, 'UTF-8') ?></code></footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
ittybittytetris/icon.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
443
ittybittytetris/index.php
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
<?php /* Simple Tetris clone in a single index.php (HTML + JS). Now with WebAudio sound effects (line clear, drop/lock, game over) + mute toggle. */ ?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Vibe Coding Test – Tetris (with SFX)</title>
|
||||||
|
<style>
|
||||||
|
:root { --bg:#0b0f14; --fg:#e6ebf0; --muted:#8a99a8; --panel:#121922; --accent:#4dd0e1; }
|
||||||
|
* { box-sizing:border-box; }
|
||||||
|
html,body { height:100%; }
|
||||||
|
body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; background:radial-gradient(1200px 700px at 20% -10%, #15202b 0, #0b0f14 60%); color:var(--fg); display:grid; place-items:center; }
|
||||||
|
.wrap { display:grid; grid-template-columns:auto 240px; gap:16px; padding:20px; }
|
||||||
|
.panel { background:var(--panel); border:1px solid #1d2631; border-radius:14px; padding:14px; box-shadow:0 10px 25px rgba(0,0,0,.25); }
|
||||||
|
canvas { display:block; background:#0c121a; border:1px solid #1d2631; border-radius:12px; box-shadow:inset 0 0 0 1px rgba(255,255,255,.03); }
|
||||||
|
h1 { margin:0 0 8px; font-size:18px; letter-spacing:.4px; color:#cfe6ff; display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
||||||
|
.stats { display:grid; gap:10px; }
|
||||||
|
.stat { display:flex; justify-content:space-between; font-variant-numeric:tabular-nums; }
|
||||||
|
.controls { margin-top:10px; font-size:13px; color:var(--muted); line-height:1.4; }
|
||||||
|
.btns { display:flex; gap:8px; margin-top:10px; flex-wrap:wrap; }
|
||||||
|
button { background:#17212b; border:1px solid #243242; color:var(--fg); padding:8px 10px; border-radius:10px; cursor:pointer; }
|
||||||
|
button:hover { border-color:#2f455a; }
|
||||||
|
.next { margin-top:10px; display:grid; grid-template-columns:repeat(4, 22px); grid-template-rows:repeat(4, 22px); gap:2px; justify-content:flex-start; }
|
||||||
|
.cell { width:22px; height:22px; background:#0c121a; border:1px solid #1a2532; border-radius:4px; }
|
||||||
|
.badge { display:inline-block; background:#13202a; border:1px solid #2a404f; padding:2px 6px; border-radius:6px; font-size:12px; color:#bfe9ff; }
|
||||||
|
.footer { margin-top:8px; font-size:12px; color:#7f8fa1; }
|
||||||
|
.muted { opacity:.6; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="panel">
|
||||||
|
<canvas id="board" width="300" height="600" aria-label="Tetris board" role="img"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="panel" style="width:240px">
|
||||||
|
<h1>
|
||||||
|
<span>TETRIS <span class="badge">vanilla JS</span></span>
|
||||||
|
<button id="btn-sound" title="Toggle sound">🔊</button>
|
||||||
|
</h1>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
|
||||||
|
<div class="stat"><span>Lines</span><strong id="lines">0</strong></div>
|
||||||
|
<div class="stat"><span>Level</span><strong id="level">1</strong></div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<div style="font-size:13px; color:#9fb3c6; margin-bottom:6px">Next</div>
|
||||||
|
<div id="next" class="next" aria-label="Next tetromino preview"></div>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<button id="btn-pause" title="P">Pause</button>
|
||||||
|
<button id="btn-restart" title="R">Restart</button>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<div>← → move, ↓ soft drop</div>
|
||||||
|
<div>↑ rotate, Space hard drop</div>
|
||||||
|
<div>P pause, R restart</div>
|
||||||
|
<div style="margin-top:6px; color:#9fb3c6">Tip: Some browsers need a key press to enable audio.</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">© <?php echo date('Y'); ?> VIBING test</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
// ====== Config ======
|
||||||
|
const COLS = 10, ROWS = 20, SIZE = 30; // canvas cell size
|
||||||
|
const GRAVITY_BASE_MS = 900; // level 1 drop interval
|
||||||
|
const LEVEL_ACCEL = 0.88; // per level speed multiplier
|
||||||
|
|
||||||
|
// Colors per piece type
|
||||||
|
const COLORS = {
|
||||||
|
I: '#66d9ff', J: '#85a5ff', L: '#ffc98b', O: '#f7f58a', S: '#9cff9c', T: '#e6a8ff', Z: '#ff9aae'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tetromino rotation states (4x4 matrices)
|
||||||
|
const SHAPES = {
|
||||||
|
I: [
|
||||||
|
[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,0,1,0],[0,0,1,0],[0,0,1,0],[0,0,1,0]],
|
||||||
|
[[0,0,0,0],[0,0,0,0],[1,1,1,1],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]]
|
||||||
|
],
|
||||||
|
J: [
|
||||||
|
[[1,0,0],[1,1,1],[0,0,0]],
|
||||||
|
[[0,1,1],[0,1,0],[0,1,0]],
|
||||||
|
[[0,0,0],[1,1,1],[0,0,1]],
|
||||||
|
[[0,1,0],[0,1,0],[1,1,0]]
|
||||||
|
],
|
||||||
|
L: [
|
||||||
|
[[0,0,1],[1,1,1],[0,0,0]],
|
||||||
|
[[0,1,0],[0,1,0],[0,1,1]],
|
||||||
|
[[0,0,0],[1,1,1],[1,0,0]],
|
||||||
|
[[1,1,0],[0,1,0],[0,1,0]]
|
||||||
|
],
|
||||||
|
O: [
|
||||||
|
[[1,1],[1,1]],
|
||||||
|
[[1,1],[1,1]],
|
||||||
|
[[1,1],[1,1]],
|
||||||
|
[[1,1],[1,1]]
|
||||||
|
],
|
||||||
|
S: [
|
||||||
|
[[0,1,1],[1,1,0],[0,0,0]],
|
||||||
|
[[0,1,0],[0,1,1],[0,0,1]],
|
||||||
|
[[0,0,0],[0,1,1],[1,1,0]],
|
||||||
|
[[1,0,0],[1,1,0],[0,1,0]]
|
||||||
|
],
|
||||||
|
T: [
|
||||||
|
[[0,1,0],[1,1,1],[0,0,0]],
|
||||||
|
[[0,1,0],[0,1,1],[0,1,0]],
|
||||||
|
[[0,0,0],[1,1,1],[0,1,0]],
|
||||||
|
[[0,1,0],[1,1,0],[0,1,0]]
|
||||||
|
],
|
||||||
|
Z: [
|
||||||
|
[[1,1,0],[0,1,1],[0,0,0]],
|
||||||
|
[[0,0,1],[0,1,1],[0,1,0]],
|
||||||
|
[[0,0,0],[1,1,0],[0,1,1]],
|
||||||
|
[[0,1,0],[1,1,0],[1,0,0]]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// ====== Helpers ======
|
||||||
|
const $ = sel => document.querySelector(sel);
|
||||||
|
const board = $('#board');
|
||||||
|
const ctx = board.getContext('2d');
|
||||||
|
board.width = COLS * SIZE; board.height = ROWS * SIZE;
|
||||||
|
|
||||||
|
const ui = {
|
||||||
|
score: $('#score'), lines: $('#lines'), level: $('#level'),
|
||||||
|
next: $('#next'), pauseBtn: $('#btn-pause'), restartBtn: $('#btn-restart'), soundBtn: $('#btn-sound')
|
||||||
|
};
|
||||||
|
|
||||||
|
function emptyMatrix(w, h, v=0) { return Array.from({length:h}, () => Array(w).fill(v)); }
|
||||||
|
|
||||||
|
// 7-bag randomizer
|
||||||
|
class Bag {
|
||||||
|
constructor(){ this.bag = []; }
|
||||||
|
next(){
|
||||||
|
if (this.bag.length === 0) this.bag = shuffle(Object.keys(SHAPES));
|
||||||
|
return this.bag.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function shuffle(a){ for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; } return a; }
|
||||||
|
|
||||||
|
// Piece class
|
||||||
|
class Piece {
|
||||||
|
constructor(type){
|
||||||
|
this.type = type;
|
||||||
|
this.rot = 0;
|
||||||
|
this.shape = SHAPES[type][0];
|
||||||
|
this.x = Math.floor((COLS - this.shape[0].length)/2);
|
||||||
|
this.y = -this.shape.length; // spawn above
|
||||||
|
}
|
||||||
|
rotate(dir){ // dir: +1 cw, -1 ccw
|
||||||
|
const oldRot = this.rot;
|
||||||
|
this.rot = (this.rot + dir + 4) % 4;
|
||||||
|
this.shape = SHAPES[this.type][this.rot];
|
||||||
|
// simple wall-kick: try offsets
|
||||||
|
const kicks = [0, -1, 1, -2, 2];
|
||||||
|
for (const dx of kicks){ if (!collide(boardState, this, this.x+dx, this.y)) { this.x += dx; sfx.tick(); return true; } }
|
||||||
|
// revert
|
||||||
|
this.rot = oldRot; this.shape = SHAPES[this.type][this.rot];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Audio (WebAudio synth) ======
|
||||||
|
class SFX {
|
||||||
|
constructor(){
|
||||||
|
this.enabled = true;
|
||||||
|
this.ctx = null;
|
||||||
|
this.started = false;
|
||||||
|
const gesture = () => { this.start(); window.removeEventListener('keydown', gesture); window.removeEventListener('pointerdown', gesture); };
|
||||||
|
window.addEventListener('keydown', gesture);
|
||||||
|
window.addEventListener('pointerdown', gesture);
|
||||||
|
}
|
||||||
|
start(){
|
||||||
|
if (this.started) return;
|
||||||
|
const AC = window.AudioContext || window.webkitAudioContext;
|
||||||
|
if (!AC) return; // no audio support
|
||||||
|
this.ctx = new AC();
|
||||||
|
this.started = true;
|
||||||
|
}
|
||||||
|
toggle(){ this.enabled = !this.enabled; updateSoundButton(); }
|
||||||
|
// tiny helper to make beeps
|
||||||
|
beep(freq=440, dur=0.08, type='square', gain=0.03, when=0){
|
||||||
|
if (!this.enabled || !this.ctx) return;
|
||||||
|
const t = this.ctx.currentTime + when;
|
||||||
|
const o = this.ctx.createOscillator();
|
||||||
|
const g = this.ctx.createGain();
|
||||||
|
o.type = type; o.frequency.setValueAtTime(freq, t);
|
||||||
|
g.gain.setValueAtTime(0.0001, t);
|
||||||
|
g.gain.exponentialRampToValueAtTime(gain, t+0.005);
|
||||||
|
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
||||||
|
o.connect(g).connect(this.ctx.destination);
|
||||||
|
o.start(t);
|
||||||
|
o.stop(t + dur + 0.02);
|
||||||
|
}
|
||||||
|
drop(){ this.beep(220, 0.06, 'sawtooth', 0.04); }
|
||||||
|
lock(){ this.beep(180, 0.07, 'triangle', 0.035); }
|
||||||
|
line(n){
|
||||||
|
const base = 520; // cheerful arpeggio
|
||||||
|
for (let i=0;i<n;i++) this.beep(base + i*120, 0.08, 'square', 0.04, i*0.05);
|
||||||
|
}
|
||||||
|
gameover(){
|
||||||
|
[440, 392, 349, 329].forEach((f,i)=> this.beep(f, 0.18, 'sawtooth', 0.035, i*0.12));
|
||||||
|
}
|
||||||
|
tick(){ this.beep(700, 0.03, 'square', 0.02); } // rotate/move
|
||||||
|
}
|
||||||
|
const sfx = new SFX();
|
||||||
|
|
||||||
|
function updateSoundButton(){
|
||||||
|
if (!ui.soundBtn) return;
|
||||||
|
ui.soundBtn.textContent = sfx.enabled ? '🔊' : '🔈';
|
||||||
|
ui.soundBtn.classList.toggle('muted', !sfx.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Game state ======
|
||||||
|
let boardState = emptyMatrix(COLS, ROWS, 0);
|
||||||
|
let bag = new Bag();
|
||||||
|
let current = new Piece(bag.next());
|
||||||
|
let nextPieceType = bag.next();
|
||||||
|
let score = 0, lines = 0, level = 1;
|
||||||
|
let dropTimer = 0, dropInterval = gravityFor(level), lastTime = 0;
|
||||||
|
let paused = false, gameOver = false;
|
||||||
|
|
||||||
|
drawNextPreview();
|
||||||
|
updateUI();
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
|
||||||
|
// ====== Core ======
|
||||||
|
function loop(time=0){
|
||||||
|
const dt = time - lastTime; lastTime = time;
|
||||||
|
if (!paused && !gameOver){
|
||||||
|
dropTimer += dt;
|
||||||
|
if (dropTimer >= dropInterval){
|
||||||
|
dropTimer = 0; softDrop(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gravityFor(level){ return GRAVITY_BASE_MS * Math.pow(LEVEL_ACCEL, Math.max(0, level-1)); }
|
||||||
|
|
||||||
|
function collide(mat, piece, ox = piece.x, oy = piece.y){
|
||||||
|
const s = piece.shape;
|
||||||
|
for (let y=0; y<s.length; y++){
|
||||||
|
for (let x=0; x<s[y].length; x++){
|
||||||
|
if (!s[y][x]) continue;
|
||||||
|
const px = ox + x, py = oy + y;
|
||||||
|
if (px < 0 || px >= COLS || py >= ROWS) return true;
|
||||||
|
if (py >= 0 && mat[py][px]) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockPiece(){
|
||||||
|
const s = current.shape;
|
||||||
|
for (let y=0; y<s.length; y++){
|
||||||
|
for (let x=0; x<s[y].length; x++){
|
||||||
|
if (s[y][x]){
|
||||||
|
const px = current.x + x, py = current.y + y;
|
||||||
|
if (py < 0){ // topped out
|
||||||
|
gameOver = true; paused = false; sfx.gameover();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boardState[py][px] = current.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sfx.lock();
|
||||||
|
const cleared = clearLines();
|
||||||
|
if (cleared > 0){
|
||||||
|
lines += cleared;
|
||||||
|
score += [0, 100, 300, 500, 800][cleared] * level;
|
||||||
|
level = 1 + Math.floor(lines / 10);
|
||||||
|
dropInterval = gravityFor(level);
|
||||||
|
sfx.line(cleared);
|
||||||
|
}
|
||||||
|
current = new Piece(nextPieceType);
|
||||||
|
nextPieceType = bag.next();
|
||||||
|
drawNextPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLines(){
|
||||||
|
let cleared = 0;
|
||||||
|
for (let y = ROWS-1; y >= 0; y--){
|
||||||
|
if (boardState[y].every(v => v)){
|
||||||
|
boardState.splice(y,1);
|
||||||
|
boardState.unshift(Array(COLS).fill(0));
|
||||||
|
cleared++; y++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleared;
|
||||||
|
}
|
||||||
|
|
||||||
|
function softDrop(fromGravity=false){
|
||||||
|
if (!collide(boardState, current, current.x, current.y+1)){
|
||||||
|
current.y++;
|
||||||
|
if (!fromGravity) sfx.drop(); // manual soft drop feedback
|
||||||
|
} else {
|
||||||
|
lockPiece();
|
||||||
|
}
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hardDrop(){
|
||||||
|
let dist = 0;
|
||||||
|
while (!collide(boardState, current, current.x, current.y+1)){
|
||||||
|
current.y++; dist++;
|
||||||
|
}
|
||||||
|
score += 2 * dist; // small bonus
|
||||||
|
sfx.drop();
|
||||||
|
lockPiece();
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function move(dx){
|
||||||
|
if (!collide(boardState, current, current.x+dx, current.y)) { current.x += dx; sfx.tick(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Rendering ======
|
||||||
|
function render(){
|
||||||
|
ctx.clearRect(0,0,board.width,board.height);
|
||||||
|
// grid (subtle)
|
||||||
|
ctx.globalAlpha = 0.05;
|
||||||
|
for(let x=0;x<COLS;x++){
|
||||||
|
for(let y=0;y<ROWS;y++){
|
||||||
|
ctx.strokeRect(x*SIZE, y*SIZE, SIZE, SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
|
||||||
|
// draw board
|
||||||
|
for (let y=0; y<ROWS; y++){
|
||||||
|
for (let x=0; x<COLS; x++){
|
||||||
|
const t = boardState[y][x];
|
||||||
|
if (!t) continue;
|
||||||
|
drawCell(x, y, COLORS[t]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw current piece
|
||||||
|
const s = current.shape;
|
||||||
|
for (let y=0; y<s.length; y++){
|
||||||
|
for (let x=0; x<s[y].length; x++){
|
||||||
|
if (!s[y][x]) continue;
|
||||||
|
const px = current.x + x, py = current.y + y;
|
||||||
|
if (py >= 0) drawCell(px, py, COLORS[current.type]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paused) overlayText('PAUSED');
|
||||||
|
if (gameOver) overlayText('GAME OVER');
|
||||||
|
}
|
||||||
|
|
||||||
|
function overlayText(text){
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
||||||
|
ctx.fillRect(0,0,board.width,board.height);
|
||||||
|
ctx.fillStyle = '#e6ebf0';
|
||||||
|
ctx.font = 'bold 28px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(text, board.width/2, board.height/2);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCell(x, y, color){
|
||||||
|
const px = x*SIZE, py = y*SIZE;
|
||||||
|
// base
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(px+1, py+1, SIZE-2, SIZE-2);
|
||||||
|
// glossy inset
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,.15)';
|
||||||
|
ctx.fillRect(px+3, py+3, SIZE-6, Math.floor((SIZE-6)/2));
|
||||||
|
// border
|
||||||
|
ctx.strokeStyle = 'rgba(0,0,0,.35)';
|
||||||
|
ctx.strokeRect(px+0.5, py+0.5, SIZE-1, SIZE-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawNextPreview(){
|
||||||
|
ui.next.innerHTML = '';
|
||||||
|
const grid = Array.from({length:16}, () => {
|
||||||
|
const d = document.createElement('div'); d.className = 'cell'; return d; });
|
||||||
|
grid.forEach(cell => ui.next.appendChild(cell));
|
||||||
|
const shape = SHAPES[nextPieceType][0];
|
||||||
|
// center in 4x4
|
||||||
|
const offX = Math.floor((4 - shape[0].length)/2);
|
||||||
|
const offY = Math.floor((4 - shape.length)/2);
|
||||||
|
for (let y=0;y<shape.length;y++){
|
||||||
|
for (let x=0;x<shape[y].length;x++){
|
||||||
|
if (shape[y][x]){
|
||||||
|
const idx = (offY+y)*4 + (offX+x);
|
||||||
|
grid[idx].style.background = COLORS[nextPieceType];
|
||||||
|
grid[idx].style.borderColor = '#2a3b4f';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI(){
|
||||||
|
ui.score.textContent = score;
|
||||||
|
ui.lines.textContent = lines;
|
||||||
|
ui.level.textContent = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Input ======
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (gameOver){ if (e.key.toLowerCase()==='r'){ restart(); } return; }
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowLeft': move(-1); break;
|
||||||
|
case 'ArrowRight': move(+1); break;
|
||||||
|
case 'ArrowDown': softDrop(); break;
|
||||||
|
case 'ArrowUp': current.rotate(+1); break;
|
||||||
|
case ' ': e.preventDefault(); hardDrop(); break;
|
||||||
|
case 'p': case 'P': togglePause(); break;
|
||||||
|
case 'r': case 'R': restart(); break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.pauseBtn.addEventListener('click', togglePause);
|
||||||
|
ui.restartBtn.addEventListener('click', restart);
|
||||||
|
ui.soundBtn.addEventListener('click', () => { sfx.toggle(); });
|
||||||
|
|
||||||
|
function togglePause(){ if (gameOver) return; paused = !paused; }
|
||||||
|
|
||||||
|
function restart(){
|
||||||
|
boardState = emptyMatrix(COLS, ROWS, 0);
|
||||||
|
bag = new Bag();
|
||||||
|
current = new Piece(bag.next());
|
||||||
|
nextPieceType = bag.next();
|
||||||
|
score = 0; lines = 0; level = 1; dropInterval = gravityFor(level); dropTimer = 0; lastTime = 0;
|
||||||
|
gameOver = false; paused = false;
|
||||||
|
drawNextPreview(); updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize sound button state
|
||||||
|
updateSoundButton();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
394
zuss/app.js
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
/* WFW-Aushang PWA
|
||||||
|
* - Listet Dateien aus public WebDAV Share
|
||||||
|
* - Öffnet PDFs über PDF.js
|
||||||
|
* - Install/Update Overlay mit beforeinstallprompt.prompt()
|
||||||
|
*/
|
||||||
|
|
||||||
|
const APP_VERSION = "2025.12.24.6";
|
||||||
|
const TOKEN = "T9e7WESBXxy6rSD";
|
||||||
|
const BASE = "https://home.x-s.at";
|
||||||
|
const SHARE_DAV = `${BASE}/public.php/dav/files/${TOKEN}`;
|
||||||
|
const PDFJS = `${BASE}/pdfjs/web/viewer.html?file=`;
|
||||||
|
|
||||||
|
// relative to /zuss/
|
||||||
|
const VERSION_URL = "./version.json";
|
||||||
|
const LIST_START_DIR = "/"; // share root
|
||||||
|
|
||||||
|
let deferredInstallPrompt = null;
|
||||||
|
let latestKnown = APP_VERSION;
|
||||||
|
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
const el = $("err");
|
||||||
|
el.style.display = "block";
|
||||||
|
el.textContent = msg;
|
||||||
|
}
|
||||||
|
function clearError() {
|
||||||
|
const el = $("err");
|
||||||
|
el.style.display = "none";
|
||||||
|
el.textContent = "";
|
||||||
|
}
|
||||||
|
function setStatus(msg) { $("status").textContent = msg; }
|
||||||
|
|
||||||
|
function showOverlay(title, text, primaryText, showPrimary = true) {
|
||||||
|
$("ovTitle").textContent = title;
|
||||||
|
$("ovText").textContent = text;
|
||||||
|
$("ovPrimaryBtn").textContent = primaryText;
|
||||||
|
$("ovPrimaryBtn").style.display = showPrimary ? "flex" : "none";
|
||||||
|
$("ovHint").style.display = "none";
|
||||||
|
$("installOverlay").style.display = "block";
|
||||||
|
}
|
||||||
|
function hideOverlay() {
|
||||||
|
$("installOverlay").style.display = "none";
|
||||||
|
}
|
||||||
|
function showManualInstallHint() {
|
||||||
|
$("ovHint").style.display = "block";
|
||||||
|
$("ovHint").textContent =
|
||||||
|
"Hinweis: Falls kein Install-Dialog erscheint: Chrome Menü (⋮) → „App installieren“ / „Zum Startbildschirm hinzufügen“.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareVersions(a, b) {
|
||||||
|
// "2025.12.24.3" style; compare numeric segments
|
||||||
|
const pa = String(a).split(".").map(n => parseInt(n, 10) || 0);
|
||||||
|
const pb = String(b).split(".").map(n => parseInt(n, 10) || 0);
|
||||||
|
const len = Math.max(pa.length, pb.length);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const da = pa[i] || 0, db = pb[i] || 0;
|
||||||
|
if (da > db) return 1;
|
||||||
|
if (da < db) return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLatestVersion() {
|
||||||
|
try {
|
||||||
|
const r = await fetch(VERSION_URL, { cache: "no-store" });
|
||||||
|
if (!r.ok) throw new Error(`version.json HTTP ${r.status}`);
|
||||||
|
const j = await r.json();
|
||||||
|
if (j && j.version) {
|
||||||
|
latestKnown = j.version;
|
||||||
|
sessionStorage.setItem("wfw_latest", latestKnown);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// not fatal
|
||||||
|
console.warn("Could not fetch version.json:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInstalledVersion() {
|
||||||
|
return localStorage.getItem("wfw_installed_version") || null;
|
||||||
|
}
|
||||||
|
function markInstalledVersion(v) {
|
||||||
|
localStorage.setItem("wfw_installed_version", v);
|
||||||
|
}
|
||||||
|
function markOverlayDismissed(v) {
|
||||||
|
localStorage.setItem("wfw_dismissed_version", v);
|
||||||
|
}
|
||||||
|
function getDismissedVersion() {
|
||||||
|
return localStorage.getItem("wfw_dismissed_version") || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkInstallOrUpdateOverlay() {
|
||||||
|
await fetchLatestVersion();
|
||||||
|
|
||||||
|
const installed = getInstalledVersion();
|
||||||
|
const dismissed = getDismissedVersion();
|
||||||
|
const isStandalone = window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true;
|
||||||
|
|
||||||
|
// If installed version < latest -> show update overlay
|
||||||
|
if (installed && compareVersions(installed, latestKnown) < 0) {
|
||||||
|
showOverlay(
|
||||||
|
"Update verfügbar",
|
||||||
|
`Neue Version verfügbar (${latestKnown}). Bitte updaten für die neuesten Fixes.`,
|
||||||
|
"Update jetzt"
|
||||||
|
);
|
||||||
|
$("ovPrimaryBtn").onclick = async () => {
|
||||||
|
try {
|
||||||
|
// ask SW to update + reload
|
||||||
|
const reg = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (reg) await reg.update();
|
||||||
|
} catch {}
|
||||||
|
markInstalledVersion(latestKnown);
|
||||||
|
location.reload();
|
||||||
|
};
|
||||||
|
$("ovContinueBtn").onclick = () => {
|
||||||
|
markOverlayDismissed(latestKnown);
|
||||||
|
hideOverlay();
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not installed (or standalone not detected), show install overlay until latest dismissed/installed
|
||||||
|
if (!installed && dismissed !== latestKnown && !isStandalone) {
|
||||||
|
showOverlay(
|
||||||
|
"WFW-Aushang",
|
||||||
|
"Installiere mich!",
|
||||||
|
"Installieren"
|
||||||
|
);
|
||||||
|
|
||||||
|
$("ovPrimaryBtn").onclick = async (e) => {
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
await doInstall();
|
||||||
|
};
|
||||||
|
$("ovContinueBtn").onclick = (e) => {
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
markOverlayDismissed(latestKnown);
|
||||||
|
hideOverlay();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doInstall() {
|
||||||
|
if (!deferredInstallPrompt) {
|
||||||
|
showManualInstallHint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
deferredInstallPrompt.prompt(); // <-- IMPORTANT
|
||||||
|
const res = await deferredInstallPrompt.userChoice;
|
||||||
|
deferredInstallPrompt = null;
|
||||||
|
|
||||||
|
if (res && res.outcome === "accepted") {
|
||||||
|
// We'll also set installed version on appinstalled event
|
||||||
|
$("ovTitle").textContent = "Installiert ✓";
|
||||||
|
$("ovText").textContent = "Danke! Du kannst die App jetzt wie eine normale App starten.";
|
||||||
|
markInstalledVersion(latestKnown);
|
||||||
|
setTimeout(() => hideOverlay(), 800);
|
||||||
|
} else {
|
||||||
|
showManualInstallHint();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Install prompt failed:", e);
|
||||||
|
showManualInstallHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WebDAV listing **/
|
||||||
|
async function propfind(dir) {
|
||||||
|
const url = `${SHARE_DAV}${dir.endsWith("/") ? dir : dir + "/"}`;
|
||||||
|
const body = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:propfind xmlns:d="DAV:">
|
||||||
|
<d:prop>
|
||||||
|
<d:displayname/>
|
||||||
|
<d:getcontenttype/>
|
||||||
|
<d:getcontentlength/>
|
||||||
|
<d:resourcetype/>
|
||||||
|
<d:getlastmodified/>
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>`;
|
||||||
|
const r = await fetch(url, {
|
||||||
|
method: "PROPFIND",
|
||||||
|
headers: { "Depth": "1", "Content-Type": "application/xml" },
|
||||||
|
body
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
const t = await r.text().catch(()=> "");
|
||||||
|
throw new Error(`PROPFIND ${r.status} ${r.statusText}\n${t.slice(0, 400)}`);
|
||||||
|
}
|
||||||
|
return await r.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDavMultistatus(xmlText, dir) {
|
||||||
|
const p = new DOMParser();
|
||||||
|
const xml = p.parseFromString(xmlText, "application/xml");
|
||||||
|
const responses = Array.from(xml.getElementsByTagNameNS("DAV:", "response"));
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
for (const resp of responses) {
|
||||||
|
const hrefEl = resp.getElementsByTagNameNS("DAV:", "href")[0];
|
||||||
|
if (!hrefEl) continue;
|
||||||
|
const href = decodeURIComponent(hrefEl.textContent || "");
|
||||||
|
|
||||||
|
// Skip the directory itself (first response)
|
||||||
|
const shareRoot = `/public.php/dav/files/${TOKEN}`;
|
||||||
|
if (href.endsWith(shareRoot + (dir === "/" ? "/" : dir))) continue;
|
||||||
|
|
||||||
|
const displayEl = resp.getElementsByTagNameNS("DAV:", "displayname")[0];
|
||||||
|
const name = displayEl ? (displayEl.textContent || "").trim() : "";
|
||||||
|
|
||||||
|
const rtEl = resp.getElementsByTagNameNS("DAV:", "resourcetype")[0];
|
||||||
|
const isCollection = rtEl && rtEl.getElementsByTagNameNS("DAV:", "collection").length > 0;
|
||||||
|
|
||||||
|
const ctEl = resp.getElementsByTagNameNS("DAV:", "getcontenttype")[0];
|
||||||
|
const contentType = ctEl ? (ctEl.textContent || "").trim() : "";
|
||||||
|
|
||||||
|
const lmEl = resp.getElementsByTagNameNS("DAV:", "getlastmodified")[0];
|
||||||
|
const lastMod = lmEl ? (lmEl.textContent || "").trim() : "";
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
href,
|
||||||
|
name: name || href.split("/").filter(Boolean).slice(-1)[0] || "",
|
||||||
|
isDir: !!isCollection,
|
||||||
|
contentType,
|
||||||
|
lastMod
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// directories first, then by name
|
||||||
|
items.sort((a,b)=> (a.isDir === b.isDir ? a.name.localeCompare(b.name) : (a.isDir ? -1 : 1)));
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPdfJsUrl(publicPath) {
|
||||||
|
// publicPath must be the share file path relative to share root, starting with "/"
|
||||||
|
const fileUrl = `${SHARE_DAV}${publicPath}`; // direct public dav file URL
|
||||||
|
return `${PDFJS}${encodeURIComponent(fileUrl)}#zoom=page-width`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList(items, currentDir) {
|
||||||
|
const list = $("list");
|
||||||
|
list.innerHTML = "";
|
||||||
|
|
||||||
|
// no folder browsing (kiosk): show only PDFs in root+subdirs? you asked "only directory content" of share root.
|
||||||
|
// We'll show only currentDir content; no folder nav UI.
|
||||||
|
const pdfs = items.filter(it => !it.isDir && (it.name.toLowerCase().endsWith(".pdf") || it.contentType.includes("pdf")));
|
||||||
|
|
||||||
|
if (pdfs.length === 0) {
|
||||||
|
setStatus("Keine PDFs gefunden.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(`PDFs: ${pdfs.length}`);
|
||||||
|
|
||||||
|
for (const it of pdfs) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.className = "row";
|
||||||
|
a.href = "#";
|
||||||
|
a.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Build relative path within share
|
||||||
|
// href includes /public.php/dav/files/<token>/...; we want path after that
|
||||||
|
const marker = `/public.php/dav/files/${TOKEN}`;
|
||||||
|
const idx = it.href.indexOf(marker);
|
||||||
|
let rel = "/";
|
||||||
|
if (idx >= 0) rel = it.href.slice(idx + marker.length);
|
||||||
|
if (!rel.startsWith("/")) rel = "/" + rel;
|
||||||
|
|
||||||
|
window.location.href = toPdfJsUrl(rel);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ico = document.createElement("div");
|
||||||
|
ico.className = "ico";
|
||||||
|
ico.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M7 3h7l3 3v15a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" stroke="currentColor" stroke-width="2" opacity=".9"/>
|
||||||
|
<path d="M14 3v4h4" stroke="currentColor" stroke-width="2" opacity=".9"/>
|
||||||
|
<path d="M7.5 14.5c2-2.5 3.5-5 4.2-7.5 1.1 3.8 2.8 7 4.8 9.5" stroke="currentColor" stroke-width="2" opacity=".9"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const mid = document.createElement("div");
|
||||||
|
const nm = document.createElement("div");
|
||||||
|
nm.className = "name";
|
||||||
|
nm.textContent = it.name;
|
||||||
|
const sub = document.createElement("div");
|
||||||
|
sub.className = "sub";
|
||||||
|
sub.textContent = it.lastMod ? it.lastMod : "";
|
||||||
|
mid.appendChild(nm);
|
||||||
|
mid.appendChild(sub);
|
||||||
|
|
||||||
|
const badge = document.createElement("div");
|
||||||
|
badge.className = "badge";
|
||||||
|
badge.textContent = "PDF";
|
||||||
|
|
||||||
|
a.appendChild(ico);
|
||||||
|
a.appendChild(mid);
|
||||||
|
a.appendChild(document.createElement("div")).className = "spacer";
|
||||||
|
a.appendChild(badge);
|
||||||
|
|
||||||
|
list.appendChild(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
clearError();
|
||||||
|
setStatus("Lade Liste…");
|
||||||
|
try {
|
||||||
|
const xml = await propfind(LIST_START_DIR);
|
||||||
|
const items = parseDavMultistatus(xml, LIST_START_DIR);
|
||||||
|
renderList(items, LIST_START_DIR);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setStatus("Fehler");
|
||||||
|
showError(String(e && e.message ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerSW() {
|
||||||
|
if (!("serviceWorker" in navigator)) return;
|
||||||
|
try {
|
||||||
|
const reg = await navigator.serviceWorker.register("./sw.js", { scope: "./" });
|
||||||
|
// Trigger update check
|
||||||
|
reg.update().catch(()=>{});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("SW register failed:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(t){
|
||||||
|
document.documentElement.setAttribute("data-theme", t);
|
||||||
|
localStorage.setItem("wfw_theme", t);
|
||||||
|
$("themeBtn").textContent = t === "dark" ? "🌙 Dunkel" : "☀️ Hell";
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTheme(){
|
||||||
|
const saved = localStorage.getItem("wfw_theme");
|
||||||
|
if (saved){
|
||||||
|
applyTheme(saved);
|
||||||
|
} else {
|
||||||
|
// Default: System
|
||||||
|
const prefersLight = window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||||
|
applyTheme(prefersLight ? "light" : "dark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener("appinstalled", () => {
|
||||||
|
// user installed the app
|
||||||
|
markInstalledVersion(latestKnown);
|
||||||
|
markOverlayDismissed(latestKnown);
|
||||||
|
hideOverlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("beforeinstallprompt", (e) => {
|
||||||
|
// We want our own overlay, so we prevent default banner
|
||||||
|
e.preventDefault();
|
||||||
|
deferredInstallPrompt = e;
|
||||||
|
// Show overlay if not yet installed
|
||||||
|
checkInstallOrUpdateOverlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extra safety: capture clicks even if something gets weird on mobile
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
const t = e.target;
|
||||||
|
if (!t) return;
|
||||||
|
|
||||||
|
if (t.id === "ovContinueBtn") {
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
markOverlayDismissed(latestKnown);
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
if (t.id === "ovPrimaryBtn") {
|
||||||
|
// If handler was overwritten, still attempt install/update
|
||||||
|
// (doInstall will show manual hint if prompt not available)
|
||||||
|
}
|
||||||
|
}, { capture: true });
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
$("refreshBtn").addEventListener("click", (e) => { e.preventDefault(); loadList(); });
|
||||||
|
$("closeBtn").addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Kiosk-ish: close tab if possible, else just go back
|
||||||
|
window.close();
|
||||||
|
setTimeout(() => { history.back(); }, 150);
|
||||||
|
});
|
||||||
|
initTheme();
|
||||||
|
$("themeBtn").addEventListener("click", () => {
|
||||||
|
const current = document.documentElement.getAttribute("data-theme") || "dark";
|
||||||
|
applyTheme(current === "dark" ? "light" : "dark");
|
||||||
|
});
|
||||||
|
await registerSW();
|
||||||
|
await checkInstallOrUpdateOverlay();
|
||||||
|
await loadList();
|
||||||
|
});
|
||||||
BIN
zuss/icons/icon-192.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
zuss/icons/icon-512.png
Normal file
|
After Width: | Height: | Size: 279 KiB |
173
zuss/index.html
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||||
|
<meta name="theme-color" content="#0b1020" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
|
||||||
|
<link rel="manifest" href="./manifest.webmanifest">
|
||||||
|
<link rel="icon" href="./icons/icon-192.png">
|
||||||
|
<link rel="apple-touch-icon" href="./icons/icon-192.png">
|
||||||
|
|
||||||
|
<title>WFW-Aushang</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root,
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
--bg:#0b1020; --card:#121a33; --text:#e8eeff; --muted:#9fb0ff;
|
||||||
|
--line:rgba(255,255,255,.12); --btn:#2b60ff; --btn2:#1c254b;
|
||||||
|
--ok:#2bff9f; --warn:#ffd36b;
|
||||||
|
}
|
||||||
|
:root[data-theme="light"]{
|
||||||
|
--bg:#f6f7fb; --card:#ffffff; --text:#0b1020; --muted:#5060a8;
|
||||||
|
--line:rgba(0,0,0,.10); --btn:#2b60ff; --btn2:#eef1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
*{ box-sizing:border-box; }
|
||||||
|
body{
|
||||||
|
margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Arial;
|
||||||
|
background:var(--bg); color:var(--text);
|
||||||
|
}
|
||||||
|
.wrap{
|
||||||
|
max-width: 860px; margin: 0 auto; padding: 16px 16px 92px;
|
||||||
|
}
|
||||||
|
h1{ font-size: 20px; margin: 8px 0 14px; letter-spacing:.3px; }
|
||||||
|
.list{
|
||||||
|
display:flex; flex-direction:column; gap:10px;
|
||||||
|
}
|
||||||
|
.row{
|
||||||
|
display:flex; align-items:center; gap:12px;
|
||||||
|
background:var(--card); border:1px solid var(--line);
|
||||||
|
border-radius:16px; padding:12px 14px;
|
||||||
|
text-decoration:none; color:inherit;
|
||||||
|
}
|
||||||
|
.row:active{ transform: scale(0.995); }
|
||||||
|
.ico{
|
||||||
|
width:38px; height:38px; border-radius:12px;
|
||||||
|
display:grid; place-items:center;
|
||||||
|
background:rgba(43,96,255,.16);
|
||||||
|
border:1px solid rgba(43,96,255,.22);
|
||||||
|
flex:0 0 auto;
|
||||||
|
}
|
||||||
|
.name{ font-size: 16px; line-height:1.2; }
|
||||||
|
.sub{ font-size: 12px; color: var(--muted); margin-top:3px; }
|
||||||
|
.spacer{ flex:1; }
|
||||||
|
.badge{
|
||||||
|
font-size:12px; color:var(--muted);
|
||||||
|
border:1px solid var(--line); padding:6px 10px; border-radius:999px;
|
||||||
|
background:rgba(0,0,0,.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer{
|
||||||
|
position:fixed; left:0; right:0; bottom:0;
|
||||||
|
padding:12px 12px calc(12px + env(safe-area-inset-bottom));
|
||||||
|
background:linear-gradient(to top, rgba(11,16,32,.95), rgba(11,16,32,.55), rgba(11,16,32,0));
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: light){
|
||||||
|
.footer{ background:linear-gradient(to top, rgba(246,247,251,.95), rgba(246,247,251,.55), rgba(246,247,251,0)); }
|
||||||
|
}
|
||||||
|
.footerInner{
|
||||||
|
max-width: 860px; margin:0 auto; display:flex; gap:10px; pointer-events:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BIG BUTTONS */
|
||||||
|
.btn{
|
||||||
|
border:0; border-radius:18px;
|
||||||
|
padding:18px 18px; /* bigger */
|
||||||
|
font-size:18px; /* bigger */
|
||||||
|
font-weight:800;
|
||||||
|
min-height:64px; /* bigger */
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
cursor:pointer;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,.22);
|
||||||
|
text-decoration:none;
|
||||||
|
user-select:none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.btnPrimary{ background:var(--btn); color:white; flex:1; }
|
||||||
|
.btnSecondary{ background:var(--btn2); color:var(--text); flex:1; border:1px solid var(--line); box-shadow:none; }
|
||||||
|
.btn:active{ transform: scale(0.995); }
|
||||||
|
|
||||||
|
.status{
|
||||||
|
margin: 8px 0 14px; color: var(--muted); font-size: 13px;
|
||||||
|
}
|
||||||
|
.error{
|
||||||
|
background: rgba(255,80,80,.12);
|
||||||
|
border:1px solid rgba(255,80,80,.25);
|
||||||
|
color: #ffb7b7;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* INSTALL/UPDATE OVERLAY */
|
||||||
|
#installOverlay{
|
||||||
|
position:fixed; inset:0;
|
||||||
|
display:none;
|
||||||
|
z-index:2147483647;
|
||||||
|
background: rgba(0,0,0,.55);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
pointer-events:auto;
|
||||||
|
padding: 18px 14px;
|
||||||
|
}
|
||||||
|
#installOverlay *{ pointer-events:auto; }
|
||||||
|
.ovCard{
|
||||||
|
max-width: 560px;
|
||||||
|
margin: 10vh auto 0;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 22px;
|
||||||
|
padding: 18px 16px;
|
||||||
|
box-shadow: 0 20px 70px rgba(0,0,0,.35);
|
||||||
|
}
|
||||||
|
.ovTitle{ font-size: 18px; font-weight: 850; margin: 6px 0 6px; }
|
||||||
|
.ovText{ color: var(--muted); font-size: 14px; line-height: 1.35; margin: 0 0 14px; }
|
||||||
|
.ovBtns{ display:flex; gap:12px; margin-top: 10px; }
|
||||||
|
.ovBtns .btn{ flex:1; }
|
||||||
|
.ovHint{
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding-top: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>WFW-Aushang</h1>
|
||||||
|
<div id="status" class="status">Lade Liste…</div>
|
||||||
|
<div id="err" class="error" style="display:none"></div>
|
||||||
|
<div id="list" class="list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<div class="footerInner">
|
||||||
|
<button id="closeBtn" class="btn btnSecondary">Aushang schließen</button>
|
||||||
|
<button id="themeBtn" class="btn btnSecondary">🌙 Dunkel</button>
|
||||||
|
<button id="refreshBtn" class="btn btnPrimary">↻ Refresh</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="installOverlay" role="dialog" aria-modal="true">
|
||||||
|
<div class="ovCard">
|
||||||
|
<div class="ovTitle" id="ovTitle">WFW-Aushang</div>
|
||||||
|
<p class="ovText" id="ovText">Installiere mich!</p>
|
||||||
|
<div class="ovBtns">
|
||||||
|
<button id="ovPrimaryBtn" class="btn btnPrimary">Installieren</button>
|
||||||
|
<button id="ovContinueBtn" class="btn btnSecondary">Weiter im Browser</button>
|
||||||
|
</div>
|
||||||
|
<div class="ovHint" id="ovHint" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./app.js?v=2025.12.24.6"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
zuss/manifest.webmanifest
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "WFW-Aushang",
|
||||||
|
"short_name": "WFW-Aushang",
|
||||||
|
"start_url": "/zuss/?source=pwa",
|
||||||
|
"scope": "/zuss/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"background_color": "#0b1020",
|
||||||
|
"theme_color": "#0b1020",
|
||||||
|
"icons": [
|
||||||
|
{ "src": "/zuss/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||||
|
{ "src": "/zuss/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
61
zuss/sw.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const CACHE = "wfw-aushang-2025.12.24.6";
|
||||||
|
|
||||||
|
const ASSETS = [
|
||||||
|
"/zuss/",
|
||||||
|
"/zuss/index.html",
|
||||||
|
"/zuss/app.js",
|
||||||
|
"/zuss/manifest.webmanifest",
|
||||||
|
"/zuss/version.json",
|
||||||
|
"/zuss/icons/icon-192.png",
|
||||||
|
"/zuss/icons/icon-512.png"
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil((async () => {
|
||||||
|
const cache = await caches.open(CACHE);
|
||||||
|
await cache.addAll(ASSETS);
|
||||||
|
self.skipWaiting();
|
||||||
|
})());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil((async () => {
|
||||||
|
const keys = await caches.keys();
|
||||||
|
await Promise.all(keys.map(k => (k === CACHE ? null : caches.delete(k))));
|
||||||
|
self.clients.claim();
|
||||||
|
})());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
const req = event.request;
|
||||||
|
const url = new URL(req.url);
|
||||||
|
|
||||||
|
// Network-first for version.json (so updates are found)
|
||||||
|
if (url.pathname.endsWith("/zuss/version.json")) {
|
||||||
|
event.respondWith((async () => {
|
||||||
|
try {
|
||||||
|
const fresh = await fetch(req, { cache: "no-store" });
|
||||||
|
const cache = await caches.open(CACHE);
|
||||||
|
cache.put(req, fresh.clone());
|
||||||
|
return fresh;
|
||||||
|
} catch {
|
||||||
|
const cached = await caches.match(req);
|
||||||
|
return cached || new Response('{"version":"2025.12.24.6"}', { headers: { "Content-Type": "application/json" } });
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache-first for app shell
|
||||||
|
if (url.pathname.startsWith("/zuss/")) {
|
||||||
|
event.respondWith((async () => {
|
||||||
|
const cached = await caches.match(req);
|
||||||
|
if (cached) return cached;
|
||||||
|
const fresh = await fetch(req);
|
||||||
|
const cache = await caches.open(CACHE);
|
||||||
|
cache.put(req, fresh.clone());
|
||||||
|
return fresh;
|
||||||
|
})());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
1
zuss/version.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{ "version": "2025.12.24.6" }
|
||||||