Files
Mario ca0e933d95
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Weiter am Taskgame gebastelt
2026-05-02 23:10:41 +02:00

944 lines
46 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neues Lock xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/time-picker.css">
<style>
.form-section {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1.25rem;
}
.form-section-title {
font-size: 0.78rem;
font-weight: 700;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.07em;
margin-bottom: 1rem;
}
.form-row {
display: flex;
flex-direction: column;
gap: 0.3rem;
margin-bottom: 0.9rem;
}
.form-row:last-child { margin-bottom: 0; }
.form-row label {
font-size: 0.88rem;
font-weight: 600;
color: var(--color-text);
}
.form-hint {
font-size: 0.78rem;
color: var(--color-muted);
margin-top: 0.1rem;
}
.form-row input[type="text"],
.form-row input[type="number"],
.form-row input[type="datetime-local"] {
width: 100%;
box-sizing: border-box;
}
.checkbox-row {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.75rem;
cursor: pointer;
}
.checkbox-row:last-child { margin-bottom: 0; }
.checkbox-row input[type="checkbox"] {
width: 1.1rem;
height: 1.1rem;
flex-shrink: 0;
cursor: pointer;
accent-color: var(--color-primary);
}
.checkbox-row label {
font-size: 0.9rem;
color: var(--color-text);
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 0.4rem;
flex-wrap: wrap;
}
.combo-wrap { position: relative; }
.combo-wrap input[type="text"] { width: 100%; box-sizing: border-box; }
.combo-dropdown {
display: none;
position: absolute;
top: calc(100% + 3px);
left: 0; right: 0;
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 8px;
max-height: 220px;
overflow-y: auto;
z-index: 200;
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
}
.combo-dropdown.open { display: block; }
.combo-option {
padding: 0.55rem 0.85rem;
cursor: pointer;
font-size: 0.9rem;
color: var(--color-text);
}
.combo-option:hover, .combo-option.active { background: var(--color-secondary); }
.combo-option .combo-hint { font-size: 0.78rem; color: var(--color-muted); margin-left: 0.4rem; }
.combo-empty { padding: 0.55rem 0.85rem; font-size: 0.85rem; color: var(--color-muted); font-style: italic; }
.inline-number { display: flex; align-items: center; gap: 0.5rem; }
.inline-number input { width: 90px !important; flex-shrink: 0; }
.inline-number span { font-size: 0.9rem; color: var(--color-text); }
.form-actions { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 0.5rem; }
.form-actions button { width: auto; padding: 0.65rem 1.5rem; }
.error-msg { color: #e74c3c; font-size: 0.85rem; margin-top: 0.4rem; display: none; }
.required-star { color: #e74c3c; margin-left: 0.15em; }
.field-error input { border-color: #e74c3c !important; }
.field-error-msg { font-size: 0.78rem; color: #e74c3c; margin-top: 0.15rem; }
/* LockControl-Auswahl */
.lockcontrol-options { display: flex; flex-direction: column; gap: 0.6rem; }
.lockcontrol-option {
display: flex; align-items: flex-start; gap: 0.7rem;
padding: 0.7rem 0.85rem;
border: 1px solid var(--color-secondary); border-radius: 8px;
cursor: pointer; transition: border-color 0.15s;
}
.lockcontrol-option:hover:not(.lc-disabled) { border-color: var(--color-primary); }
.lockcontrol-option.lc-selected { border-color: var(--color-primary); background: rgba(var(--color-primary-rgb, 180,80,255),0.06); }
.lockcontrol-option.lc-disabled { opacity: 0.55; cursor: not-allowed; }
.lockcontrol-option input[type="radio"] {
margin-top: 0.15rem; flex-shrink: 0;
accent-color: var(--color-primary); width: 1rem; height: 1rem; cursor: pointer;
}
.lockcontrol-option.lc-disabled input[type="radio"] { cursor: not-allowed; }
.lc-label { font-size: 0.9rem; font-weight: 600; color: var(--color-text); }
.lc-desc { font-size: 0.78rem; color: var(--color-muted); margin-top: 0.15rem; }
.lc-badge {
display: inline-block; font-size: 0.68rem; font-weight: 700;
padding: 0.15em 0.5em; border-radius: 4px;
background: var(--color-primary); color: #fff;
margin-left: 0.4rem; vertical-align: middle; letter-spacing: 0.03em;
}
/* Unlock-Code-Modal */
.modal-overlay {
display: none; position: fixed; inset: 0; z-index: 500;
align-items: center; justify-content: center;
}
.modal-overlay.open { display: flex; }
.modal-bg { position: absolute; inset: 0; background: rgba(0,0,0,0.55); }
.modal-box {
position: relative; background: var(--color-card);
border: 1px solid var(--color-secondary); border-radius: 12px;
padding: 1.5rem 1.5rem 1.25rem; max-width: 380px; width: 90%;
display: flex; flex-direction: column; align-items: center; gap: 0.75rem; z-index: 1;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h2 style="margin-bottom:1.25rem;">🔒 Neues Lock</h2>
<!-- Vorlage (Pflichtfeld) -->
<div class="form-section">
<div class="form-section-title">Vorlage<span class="required-star">*</span></div>
<div class="form-row" id="rowTemplate">
<div class="combo-wrap" id="templateCombo">
<input type="text" id="templateInput" placeholder="Vorlage suchen…" autocomplete="off">
<div class="combo-dropdown" id="templateDropdown"></div>
<input type="hidden" id="templateValue">
</div>
</div>
</div>
<!-- Personen -->
<div class="form-section">
<div class="form-section-title">Personen</div>
<div class="form-row" id="rowLockee">
<label for="lockeeInput">Lockee<span class="required-star">*</span></label>
<div class="combo-wrap" id="lockeeCombo">
<input type="text" id="lockeeInput" placeholder="Suchen oder „Ich selbst"" autocomplete="off">
<div class="combo-dropdown" id="lockeeDropdown"></div>
<input type="hidden" id="lockeeValue">
</div>
<div class="form-hint">Wähle dich selbst oder einen Freund als Lockee.</div>
</div>
<div class="checkbox-row" id="rowDetailsVisible" style="display:none;">
<input type="checkbox" id="lockeeDetailsVisible" checked>
<label for="lockeeDetailsVisible">Details für Lockee sichtbar
<span class="form-hint">(Lockee sieht die Lock-Konfiguration vor dem Annehmen)</span>
</label>
</div>
<div class="form-row" id="rowKeyholder">
<label for="keyholderInput">Keyholder*In</label>
<div class="combo-wrap" id="keyholderCombo">
<input type="text" id="keyholderInput" placeholder="Freund suchen…" autocomplete="off">
<div class="combo-dropdown" id="keyholderDropdown"></div>
<input type="hidden" id="keyholderValue">
</div>
<div class="form-hint">Ohne Keyholder läuft das Lock als Self-Lock.</div>
</div>
</div>
<!-- Optionen -->
<div class="form-section">
<div class="form-section-title">Optionen</div>
<!-- CardLock: Längste Dauer -->
<div class="form-row" id="rowMaxDuration">
<label>Längste Dauer</label>
<div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('dur',-1,'d')"></button><input type="text" id="dur_d" value="0" readonly><button type="button" onclick="tpChange('dur',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('dur',-1,'h')"></button><input type="text" id="dur_h" value="00" readonly><button type="button" onclick="tpChange('dur',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
</div>
<div class="form-hint">Das Lock öffnet spätestens nach dieser Zeit automatisch. 0 : 00 = keine Begrenzung.</div>
</div>
<!-- TimeLock: Dauer-Info aus Vorlage -->
<div class="form-row" id="rowTimeLockInfo" style="display:none;">
<label>Sperrdauer</label>
<div id="timeLockDurationText" style="font-size:0.9rem;color:var(--color-text);padding:0.3rem 0;"></div>
<div class="form-hint">Die Dauer wird beim Lock-Start zufällig aus dem Bereich der Vorlage gewählt.</div>
</div>
<!-- LockControl-Auswahl -->
<div class="form-row" id="rowLockControl">
<label>Schloss-Steuerung</label>
<div class="lockcontrol-options">
<label class="lockcontrol-option lc-selected" id="lcOptUnlockCode" onclick="selectLockControl('UNLOCK_CODE')">
<input type="radio" name="lockControl" value="UNLOCK_CODE" checked>
<div>
<div class="lc-label">🔢 Unlock-Code</div>
<div class="lc-desc">Ein numerischer Code wird generiert, den du in deinen Tresor einstellst.</div>
</div>
</label>
<label class="lockcontrol-option" id="lcOptTrust" onclick="selectLockControl('TRUST')">
<input type="radio" name="lockControl" value="TRUST">
<div>
<div class="lc-label">🤝 Trust</div>
<div class="lc-desc">Kein technisches Schloss du vertraust dir selbst oder deiner Keyholder*in.</div>
</div>
</label>
<label class="lockcontrol-option lc-disabled" id="lcOptTtlock" onclick="selectLockControl('TTLOCK')">
<input type="radio" name="lockControl" value="TTLOCK" disabled>
<div>
<div class="lc-label">📱 TTLock <span class="lc-badge" id="lcTtlockBadge">ABO</span></div>
<div class="lc-desc" id="lcTtlockDesc">Steuert ein TTLock-Smartschloss direkt über die App-Integration. Erfordert ein aktives Abonnement.</div>
</div>
</label>
</div>
</div>
<div class="form-row" id="rowUnlockCodeLines">
<label for="unlockCodeLines">Anzahl Ziffern des Entsperrcodes</label>
<div class="inline-number">
<input type="number" id="unlockCodeLines" min="1" max="20" value="5">
<span>Ziffern</span>
</div>
</div>
<div class="checkbox-row" id="rowTestLock">
<input type="checkbox" id="testLock" onchange="onTestLockChange()">
<label for="testLock">Test-Lock <span class="form-hint">(kein echter Lock, zum Ausprobieren)</span></label>
</div>
<div id="rowSpeedFactor" style="display:none; align-items:center; gap:12px; padding:6px 0;">
<label for="speedFactor" style="white-space:nowrap;">Geschwindigkeit:</label>
<input type="range" id="speedFactor" min="1" max="10" value="1" style="flex:1;" oninput="document.getElementById('speedFactorLabel').textContent = '×' + this.value">
<span id="speedFactorLabel" style="min-width:32px; text-align:right;">×1</span>
</div>
</div>
<div class="error-msg" id="errorMsg"></div>
<div class="form-actions">
<button onclick="history.back()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">Abbrechen</button>
<button onclick="createSession()">🔒 Lock starten</button>
</div>
</div>
</div>
<!-- TTLock Lade-Overlay -->
<div class="modal-overlay" id="ttlLoadingOverlay">
<div class="modal-bg"></div>
<div class="modal-box" style="max-width:320px;text-align:center;gap:0.75rem;">
<div style="font-size:2rem;"></div>
<div style="font-weight:600;">TTLock-Kommunikation läuft…</div>
<div style="font-size:0.85rem;color:var(--color-muted);">Bitte warten, der TTLock-Server wird kontaktiert.</div>
</div>
</div>
<!-- Bereits in einem Lock-Dialog -->
<div class="modal-overlay" id="activeLockModal">
<div class="modal-bg" onclick="document.getElementById('activeLockModal').classList.remove('open')"></div>
<div class="modal-box">
<div style="font-size:2rem;">🔒</div>
<h3 style="margin:0;text-align:center;">Du bist bereits in einem Lock</h3>
<p style="color:var(--color-muted);font-size:0.85rem;text-align:center;margin:0;">
Es ist bereits ein aktives Keuschheitslock für dein Konto vorhanden.
Beende oder öffne zuerst das bestehende Lock.
</p>
<a id="activeLockLink" href="#" style="display:none;width:100%;">
<button style="width:100%;">Zum aktiven Lock →</button>
</a>
<button class="btn-secondary" style="width:100%;"
onclick="document.getElementById('activeLockModal').classList.remove('open')">Schließen</button>
</div>
</div>
<!-- Entsperrcode-Modal -->
<div class="modal-overlay" id="unlockModal">
<div class="modal-bg"></div>
<div class="modal-box">
<div style="font-size:2rem;">🔒</div>
<h3 id="unlockModalTitle" style="margin:0;">Dein Entsperrcode</h3>
<p id="unlockModalHint" style="color:var(--color-muted);font-size:0.85rem;text-align:center;margin:0;">
Stelle die Kombination deines Tresors auf den folgenden Code ein und verschließe deinen Schlüssel in diesem.
</p>
<div id="unlockCodeDisplay" style="
font-family: monospace; font-size: 2rem; letter-spacing: 0.3em;
background: var(--color-secondary); border-radius: 8px;
padding: 1rem 1.5rem; text-align: center; color: var(--color-primary);
line-height: 1.8; word-break: break-all;
"></div>
<div id="unlockModalCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);text-align:center;font-family:monospace;"></div>
<div id="unlockKeyholderHint" style="display:none;background:var(--color-secondary);border-radius:8px;padding:0.75rem 1rem;font-size:0.85rem;color:var(--color-muted);text-align:center;line-height:1.5;">
⏳ Die eingetragene Keyholder*In wurde benachrichtigt und muss die Rolle noch bestätigen.
Bis zur Bestätigung läuft das Lock als Self-Lock.
</div>
<button id="unlockModalBtn" onclick="" style="width:100%;margin-top:0.25rem;">Weiter</button>
</div>
</div>
<script src="/js/card-defs.js"></script>
<script src="/js/shared.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/social-sidebar.js"></script>
<script src="/js/time-picker.js"></script>
<script>
let myUserId = null;
let myUserName = null;
let allFriends = [];
let allTemplates = []; // combined; each entry has _type: 'cardlock'|'timelock'
let selectedTemplate = null;
let comboActiveIdx = -1;
let selectedLockControl = 'UNLOCK_CODE';
let hasPaidSubscription = false;
let ttlockReady = false;
// ── Boot ──
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
if (!user) { window.location.href = '/login.html'; return; }
myUserId = user.userId;
myUserName = user.name;
// Subscription + Templates (eigene + abonnierte) + TTLock-Config parallel laden
try {
const [ownTpls, subTpls, subData, ttlCfg] = await Promise.all([
fetch('/templates/mine').then(r => r.ok ? r.json() : []),
fetch('/templates/subscribed').then(r => r.ok ? r.json() : []),
fetch('/subscription/me').then(r => r.ok ? r.json() : null),
fetch('/user/me/ttlock').then(r => r.ok ? r.json() : null)
]);
const toEntry = t => ({ ...t, _type: t.lockType === 'TIMELOCK' ? 'timelock' : 'cardlock' });
const ownIds = new Set(ownTpls.map(t => t.templateId));
allTemplates = [
...ownTpls.map(toEntry),
...subTpls.filter(t => !ownIds.has(t.templateId)).map(toEntry)
];
hasPaidSubscription = !!(subData && subData.subscriptionType === 'PREMIUM');
ttlockReady = !!(ttlCfg && ttlCfg.testSuccessful);
const ttlockTestOk = ttlockReady;
if (hasPaidSubscription && ttlockTestOk) {
const opt = document.getElementById('lcOptTtlock');
opt.classList.remove('lc-disabled');
opt.querySelector('input').disabled = false;
document.getElementById('lcTtlockBadge').style.display = 'none';
document.getElementById('lcTtlockDesc').textContent =
'Steuert ein TTLock-Smartschloss direkt über die App-Integration.';
} else if (hasPaidSubscription && !ttlockTestOk) {
document.getElementById('lcTtlockBadge').textContent = 'KONFIG';
document.getElementById('lcTtlockDesc').textContent =
'TTLock ist noch nicht konfiguriert. Bitte teste die Verbindung zuerst in den Einstellungen.';
}
} catch { allTemplates = []; }
if (allTemplates.length === 0) {
document.querySelector('.content').innerHTML = `
<div style="text-align:center;padding:3rem 1rem;">
<div style="font-size:2.5rem;margin-bottom:1rem;">📋</div>
<h2 style="margin-bottom:0.75rem;">Keine Vorlagen vorhanden</h2>
<p style="color:var(--color-muted);margin-bottom:2rem;">
Du musst zuerst mindestens eine Lock-Vorlage erstellen,<br>
bevor du ein neues Lock starten kannst.
</p>
<a href="/games/chastity/meine-locks.html" style="display:inline-block;padding:0.7rem 1.8rem;background:var(--color-primary);color:#fff;border-radius:8px;text-decoration:none;font-weight:600;">Vorlage erstellen</a>
</div>`;
return;
}
setupTemplateCombo();
await loadOptions(user.userId);
});
async function loadOptions(myId) {
try {
allFriends = await fetch('/social/friends/user/' + myId).then(r => r.ok ? r.json() : []);
} catch { allFriends = []; }
setupLockeeCombo();
setupKeyholderCombo();
document.getElementById('lockeeInput').value = 'Ich selbst';
document.getElementById('lockeeValue').value = myId;
}
// ── Template-Combobox ──
function setupTemplateCombo() {
const input = document.getElementById('templateInput');
const dropdown = document.getElementById('templateDropdown');
const hidden = document.getElementById('templateValue');
function renderDropdown(query) {
const q = query.toLowerCase().trim();
const filtered = q
? allTemplates.filter(t => (t.name || '').toLowerCase().includes(q))
: allTemplates;
dropdown.innerHTML = '';
if (filtered.length === 0) {
dropdown.innerHTML = `<div class="combo-empty">Keine Vorlagen gefunden.</div>`;
} else {
filtered.forEach(t => {
const badge = t._type === 'timelock' ? '⏱' : '🃏';
const label = (t.name || 'Unbenannte Vorlage');
const div = document.createElement('div');
div.className = 'combo-option';
div.dataset.id = t.templateId;
div.innerHTML = `${badge} ${label}`;
div.addEventListener('mousedown', e => {
e.preventDefault();
hidden.value = t.templateId;
input.value = badge + ' ' + label;
selectedTemplate = t;
dropdown.classList.remove('open');
clearFieldError('rowTemplate');
onTemplateChanged(t);
});
dropdown.appendChild(div);
});
}
dropdown.classList.add('open');
}
input.addEventListener('input', () => { hidden.value = ''; renderDropdown(input.value); });
input.addEventListener('focus', () => renderDropdown(input.value));
input.addEventListener('blur', () => {
setTimeout(() => {
dropdown.classList.remove('open');
if (!hidden.value) input.value = '';
}, 150);
});
}
// ── Lockee-Combobox ──
function setupLockeeCombo() {
const input = document.getElementById('lockeeInput');
const dropdown = document.getElementById('lockeeDropdown');
const hidden = document.getElementById('lockeeValue');
function renderDropdown(query) {
const q = query.toLowerCase().trim();
const selfMatch = 'ich selbst'.includes(q);
const filtered = allFriends.filter(f => f.name.toLowerCase().includes(q));
dropdown.innerHTML = '';
comboActiveIdx = -1;
if (!selfMatch && filtered.length === 0) {
dropdown.innerHTML = `<div class="combo-empty">${q ? 'Keine Treffer.' : 'Keine Freunde vorhanden.'}</div>`;
} else {
if (selfMatch) {
const div = document.createElement('div');
div.className = 'combo-option';
div.dataset.id = myUserId; div.dataset.name = 'Ich selbst';
div.innerHTML = 'Ich selbst<span class="combo-hint">(Self-Lock)</span>';
div.addEventListener('mousedown', e => { e.preventDefault(); selectLockee(myUserId, 'Ich selbst'); });
dropdown.appendChild(div);
}
filtered.forEach(f => {
const div = document.createElement('div');
div.className = 'combo-option';
div.dataset.id = f.userId; div.dataset.name = f.name;
div.textContent = f.name;
div.addEventListener('mousedown', e => { e.preventDefault(); selectLockee(f.userId, f.name); });
dropdown.appendChild(div);
});
}
dropdown.classList.add('open');
}
function selectLockee(id, name) {
hidden.value = id;
input.value = name;
dropdown.classList.remove('open');
onLockeeChanged(id);
}
input.addEventListener('input', () => { hidden.value = ''; renderDropdown(input.value); });
input.addEventListener('focus', () => renderDropdown(input.value));
input.addEventListener('blur', () => {
setTimeout(() => {
dropdown.classList.remove('open');
if (!hidden.value) { input.value = 'Ich selbst'; hidden.value = myUserId; onLockeeChanged(myUserId); }
}, 150);
});
}
function onLockeeChanged(lockeeId) {
const isFriend = lockeeId && lockeeId !== myUserId;
const khInput = document.getElementById('keyholderInput');
const khHidden = document.getElementById('keyholderValue');
if (isFriend) {
khInput.value = myUserName || 'Ich selbst';
khHidden.value = myUserId;
khInput.readOnly = true;
khInput.style.opacity = '0.6';
document.getElementById('rowTestLock').style.display = 'none';
document.getElementById('rowSpeedFactor').style.display = 'none';
document.getElementById('rowDetailsVisible').style.display = '';
} else {
khInput.readOnly = false;
khInput.style.opacity = '';
if (!khHidden.value) khInput.value = '';
document.getElementById('rowTestLock').style.display = '';
document.getElementById('rowDetailsVisible').style.display = 'none';
}
updateCodeLinesVisibility();
}
// Self-Lock-Felder beim Start ausblenden (werden durch onLockeeChanged gesetzt)
document.getElementById('rowTestLock').style.display = '';
// ── Keyholder-Combobox ──
function setupKeyholderCombo() {
const input = document.getElementById('keyholderInput');
const dropdown = document.getElementById('keyholderDropdown');
const hidden = document.getElementById('keyholderValue');
function renderDropdown(query) {
if (input.readOnly) return;
const q = query.toLowerCase().trim();
const filtered = q ? allFriends.filter(f => f.name.toLowerCase().includes(q)) : allFriends;
dropdown.innerHTML = '';
comboActiveIdx = -1;
if (filtered.length === 0) {
dropdown.innerHTML = `<div class="combo-empty">${q ? 'Keine Freunde gefunden.' : 'Keine Freunde vorhanden.'}</div>`;
} else {
filtered.forEach(f => {
const div = document.createElement('div');
div.className = 'combo-option';
div.dataset.id = f.userId; div.dataset.name = f.name;
div.textContent = f.name;
div.addEventListener('mousedown', e => { e.preventDefault(); hidden.value = f.userId; input.value = f.name; dropdown.classList.remove('open'); });
dropdown.appendChild(div);
});
}
dropdown.classList.add('open');
}
input.addEventListener('input', () => { hidden.value = ''; renderDropdown(input.value); });
input.addEventListener('focus', () => renderDropdown(input.value));
input.addEventListener('blur', () => { setTimeout(() => { dropdown.classList.remove('open'); if (!hidden.value) input.value = ''; }, 150); });
}
// ── Template-Typ: Sektionen umschalten ──
function onTemplateChanged(t) {
const isTimeLock = t._type === 'timelock';
document.getElementById('rowMaxDuration').style.display = isTimeLock ? 'none' : '';
document.getElementById('rowTimeLockInfo').style.display = isTimeLock ? '' : 'none';
if (isTimeLock) {
const minM = t.minTimeInMinutes || 0;
const maxM = t.maxTimeInMinutes || 0;
document.getElementById('timeLockDurationText').textContent =
`${fmtMinutes(minM)} ${fmtMinutes(maxM)}`;
}
}
function fmtMinutes(m) {
if (!m) return '0 Min.';
const d = Math.floor(m / 1440);
const h = Math.floor((m % 1440) / 60);
const min = m % 60;
const parts = [];
if (d) parts.push(d + ' Tag' + (d !== 1 ? 'e' : ''));
if (h) parts.push(h + ' Std');
if (min) parts.push(min + ' Min.');
return parts.join(' ') || '0 Min.';
}
// ── LockControl-Auswahl ──
function selectLockControl(type) {
const ids = { UNLOCK_CODE: 'lcOptUnlockCode', TRUST: 'lcOptTrust', TTLOCK: 'lcOptTtlock' };
if (type === 'TTLOCK' && (!hasPaidSubscription || !ttlockReady)) return;
selectedLockControl = type;
Object.entries(ids).forEach(([t, id]) => {
const el = document.getElementById(id);
if (!el) return;
el.classList.toggle('lc-selected', t === type);
el.querySelector('input').checked = (t === type);
});
updateCodeLinesVisibility();
}
function updateCodeLinesVisibility() {
const show = selectedLockControl === 'UNLOCK_CODE' || selectedLockControl === 'TTLOCK';
const lockeeIsFriend = document.getElementById('lockeeValue').value !== myUserId
&& !!document.getElementById('lockeeValue').value;
document.getElementById('rowUnlockCodeLines').style.display = (show && !lockeeIsFriend) ? '' : 'none';
// Label je nach Typ anpassen
const label = document.querySelector('#rowUnlockCodeLines > label');
if (label) {
label.textContent = selectedLockControl === 'TTLOCK'
? 'PIN-Länge (49 Ziffern)'
: 'Anzahl Ziffern des Entsperrcodes';
}
// Für TTLock: min=4, max=9; Standard: min=1, max=20
const input = document.getElementById('unlockCodeLines');
if (selectedLockControl === 'TTLOCK') {
input.min = 4; input.max = 9;
if (parseInt(input.value) < 4) input.value = 6;
if (parseInt(input.value) > 9) input.value = 9;
} else {
input.min = 1; input.max = 20;
}
}
// ── Längste Dauer → LocalDateTime ──
function durationToLatestOpening() {
const days = parseInt(document.getElementById('dur_d').value) || 0;
const hours = parseInt(document.getElementById('dur_h').value) || 0;
if (days === 0 && hours === 0) return null;
const ms = (days * 24 * 3600 + hours * 3600) * 1000;
const dt = new Date(Date.now() + ms);
const pad = n => String(n).padStart(2, '0');
return `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())}T${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`;
}
// ── Fehler ──
function showError(msg) {
const el = document.getElementById('errorMsg');
el.textContent = msg;
el.style.display = '';
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
async function showActiveLockError() {
const modal = document.getElementById('activeLockModal');
const linkEl = document.getElementById('activeLockLink');
linkEl.style.display = 'none';
// Eigenes aktives Lock suchen (CardLock oder TimeLock)
try {
const [cardRes, timeRes] = await Promise.all([
fetch('/keyholder/mylock'),
fetch('/keyholder/timelock/mylock')
]);
if (cardRes.status === 200) {
const d = await cardRes.json();
linkEl.href = '/games/chastity/activelock.html?lockId=' + d.lockId;
linkEl.style.display = '';
} else if (timeRes.status === 200) {
const d = await timeRes.json();
linkEl.href = '/games/chastity/activetimelock.html?lockId=' + d.lockId;
linkEl.style.display = '';
}
} catch (_) {}
modal.classList.add('open');
}
function setFieldError(rowId, msg) {
const row = document.getElementById(rowId);
if (!row) return;
row.classList.add('field-error');
let el = row.querySelector('.field-error-msg');
if (!el) { el = document.createElement('div'); el.className = 'field-error-msg'; row.appendChild(el); }
el.textContent = msg;
}
function clearFieldError(rowId) {
const row = document.getElementById(rowId);
if (!row) return;
row.classList.remove('field-error');
row.querySelector('.field-error-msg')?.remove();
}
// ── Karten aus Template aufbauen ──
function buildInitialCardsFromTemplate(t) {
const cards = [];
CARD_DEFS.forEach(c => {
const minVal = t.cardCountsMin?.[c.id] ?? 0;
const maxVal = t.cardCountsMax?.[c.id] ?? 0;
const n = minVal + Math.floor(Math.random() * (maxVal - minVal + 1));
for (let i = 0; i < n; i++) cards.push(c.id);
});
return cards;
}
// ── Plausibilitätsprüfung für TimeLock ──
function validateTimeLockPlausibility(t) {
const errors = [];
const hasTasks = t.tasks && t.tasks.length > 0;
const spinEntries = t.spinningWheelEntries || [];
// Spinning Wheel enthält Task-Felder, aber keine Aufgaben definiert
if (spinEntries.some(e => e.type === 'TASK') && !hasTasks) {
errors.push('Das Spinning Wheel enthält Aufgaben-Felder (TASK), aber die Vorlage hat keine Aufgaben definiert. Bitte die Vorlage bearbeiten.');
}
// Aufgaben-Häufigkeit konfiguriert, aber keine Aufgaben vorhanden
if ((t.taskEveryMinutes > 0 || t.minTasksPerDay > 0) && !hasTasks) {
errors.push('Aufgaben sind zeitlich konfiguriert, aber keine Aufgaben in der Vorlage definiert. Bitte die Vorlage bearbeiten.');
}
// Unbegrenztes Einfrieren ohne Auftau-Eintrag
if (spinEntries.some(e => e.type === 'FREEZE') && !spinEntries.some(e => e.type === 'UNFREEZE')) {
errors.push('Das Spinning Wheel enthält ein unbegrenztes Einfrieren (FREEZE), aber keinen Auftau-Eintrag (UNFREEZE). Das Lock könnte dauerhaft eingefroren bleiben.');
}
return errors;
}
function showPlausibilityErrors(errors) {
const el = document.getElementById('errorMsg');
if (errors.length === 1) {
el.textContent = errors[0];
} else {
el.innerHTML = 'Die Vorlage enthält inkonsistente Einstellungen:<ul style="margin:0.4rem 0 0 1.2rem;padding:0;">'
+ errors.map(e => `<li>${e}</li>`).join('')
+ '</ul>';
}
el.style.display = '';
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function onTestLockChange() {
const checked = document.getElementById('testLock').checked;
document.getElementById('rowSpeedFactor').style.display = checked ? 'flex' : 'none';
if (!checked) {
document.getElementById('speedFactor').value = 1;
document.getElementById('speedFactorLabel').textContent = '×1';
}
}
// ── Absenden ──
async function createSession() {
document.getElementById('errorMsg').style.display = 'none';
const templateId = document.getElementById('templateValue').value;
if (!templateId) {
setFieldError('rowTemplate', 'Bitte eine Vorlage wählen.');
document.getElementById('rowTemplate').scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
clearFieldError('rowTemplate');
const t = selectedTemplate || allTemplates.find(x => x.templateId === templateId);
if (!t) { showError('Vorlage nicht gefunden.'); return; }
if (t._type === 'timelock') {
const plausErrors = validateTimeLockPlausibility(t);
if (plausErrors.length > 0) {
showPlausibilityErrors(plausErrors);
return;
}
}
const lockeeVal = document.getElementById('lockeeValue').value;
const keyholderVal = document.getElementById('keyholderValue').value;
const isFriendLockee = lockeeVal && lockeeVal !== myUserId;
const unlockCodeLen = isFriendLockee ? null : (parseInt(document.getElementById('unlockCodeLines').value) || 5);
const isTestLock = isFriendLockee ? false : document.getElementById('testLock').checked;
const speedFactor = isTestLock ? parseInt(document.getElementById('speedFactor').value) : 1;
let endpoint, body;
if (t._type === 'timelock') {
endpoint = '/keyholder/timelock';
body = {
templateId: t.templateId,
lockeeUserId: isFriendLockee ? lockeeVal : null,
lockeeDetailsVisible: isFriendLockee ? document.getElementById('lockeeDetailsVisible').checked : false,
keyholder: isFriendLockee ? null : (keyholderVal || null),
testLock: isTestLock,
unlockCodeLength: unlockCodeLen,
controllType: selectedLockControl,
speedFactor: speedFactor,
};
} else {
// CardLock
const initialCards = buildInitialCardsFromTemplate(t);
if (initialCards.length === 0) {
showError('Die gewählte Vorlage enthält keine Karten. Bitte Vorlage prüfen.');
return;
}
endpoint = '/keyholder/cardlock';
body = {
name: t.name,
lockeeUserId: isFriendLockee ? lockeeVal : null,
lockeeDetailsVisible: isFriendLockee ? document.getElementById('lockeeDetailsVisible').checked : false,
keyholder: isFriendLockee ? null : (keyholderVal || null),
initialCards,
pickEveryMinute: t.pickEveryMinute,
accumulatePicks: t.accumulatePicks,
showRemainingCards: t.showRemainingCards,
latestOpeningtime: durationToLatestOpening(),
hygineOpeningEveryMinites: t.hygineOpeningEveryMinites || null,
hygineOpeningDurationMinutes: t.hygineOpeningDurationMinutes || null,
tasks: t.tasks || [],
taskCardMode: t.taskCardMode || 'RANDOM',
unlockCodeLines: unlockCodeLen,
requiresVerification: t.requiresVerification,
testLock: isTestLock,
controllType: selectedLockControl,
gameSetId: t.gameSetId || null,
gameSpieldauerIdx: t.gameSpieldauerIdx ?? null,
speedFactor: speedFactor,
};
}
if (selectedLockControl === 'TTLOCK') {
document.getElementById('ttlLoadingOverlay').classList.add('open');
}
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
document.getElementById('ttlLoadingOverlay').classList.remove('open');
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
if (res.status === 409 && errData.error === 'active_lock_exists') {
showActiveLockError();
} else if (res.status === 403 && errData.error === 'subscription_required') {
showError('TTLock erfordert ein aktives Abonnement.');
} else if (res.status === 400) {
showError('Ungültige Eingabe. Bitte alle Felder prüfen.');
} else {
showError('Fehler beim Erstellen des Locks.');
}
return;
}
const data = await res.json();
if (data.lockeeInvitationSent) {
window.location.href = '/games/common/einladungen.html?tab=gesendet';
} else if (!data.unlockCode) {
// Trust: kein Code, direkt weiter
const isTimeLock = selectedTemplate && selectedTemplate._type === 'timelock';
window.location.href = (isTimeLock ? '/games/chastity/activetimelock.html' : '/games/chastity/activelock.html')
+ '?lockId=' + data.lockId + (data.keyholderPending ? '&keyholderPending=1' : '');
} else if (selectedLockControl === 'TTLOCK') {
showTtlockStartModal(data.unlockCode, data.lockId, data.keyholderPending);
} else {
showUnlockCodeModal(data.unlockCode, data.lockId, data.keyholderPending);
}
}
// ── TTLock-Startmodal (kein Scramble, stattdessen Relock im Hintergrund) ──
function showTtlockStartModal(code, lockId, keyholderPending) {
const isTimeLock = selectedTemplate && selectedTemplate._type === 'timelock';
const lockType = isTimeLock ? 'timelock' : 'cardlock';
const targetUrl = (isTimeLock ? '/games/chastity/activetimelock.html' : '/games/chastity/activelock.html')
+ '?lockId=' + lockId + (keyholderPending ? '&keyholderPending=1' : '');
document.getElementById('unlockCodeDisplay').textContent = code;
document.getElementById('unlockModalTitle').textContent = 'Dein Startcode';
document.getElementById('unlockModalHint').textContent =
"Öffne das TTLock mit dem Code, lege den Schlüssel in das TTLock und verschließe es anschließend wieder. Der Code verliert anschließend seine Gültigkeit";
if (keyholderPending) document.getElementById('unlockKeyholderHint').style.display = '';
const btn = document.getElementById('unlockModalBtn');
btn.textContent = "🔒 Los geht's";
btn.onclick = async () => {
btn.disabled = true;
document.getElementById('unlockModal').classList.remove('open');
document.getElementById('ttlLoadingOverlay').classList.add('open');
try {
await fetch(`/keyholder/${lockType}/${lockId}/relock`, { method: 'POST' });
} catch { /* Fehler ignorieren Weiterleitung trotzdem */ }
window.location.href = targetUrl;
};
document.getElementById('unlockModal').classList.add('open');
}
// ── Entsperrcode-Modal ──
function showUnlockCodeModal(code, lockId, keyholderPending) {
document.getElementById('unlockCodeDisplay').textContent = code;
if (keyholderPending) document.getElementById('unlockKeyholderHint').style.display = '';
const isTimeLock = selectedTemplate && selectedTemplate._type === 'timelock';
const targetPage = isTimeLock ? '/games/chastity/activetimelock.html' : '/games/chastity/activelock.html';
const url = targetPage + '?lockId=' + lockId + (keyholderPending ? '&keyholderPending=1' : '');
document.getElementById('unlockModalBtn').onclick = () => startCodeScramble(code, url);
document.getElementById('unlockModal').classList.add('open');
}
function startCodeScramble(realCode, url) {
const display = document.getElementById('unlockCodeDisplay');
const btn = document.getElementById('unlockModalBtn');
const hint = document.getElementById('unlockModalHint');
const countdown = document.getElementById('unlockModalCountdown');
const len = realCode.length;
const DURATION = 3 * 60;
let remaining = DURATION;
let stopped = false;
function randomCode() {
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
}
function finish() {
stopped = true;
clearInterval(scrambleInterval);
clearInterval(countdownInterval);
window.location.href = url;
}
if (hint) hint.style.display = 'none';
countdown.style.display = '';
document.getElementById('unlockModalTitle').textContent = 'Nun vergessen wir den Code…';
btn.textContent = 'Abbrechen';
btn.onclick = finish;
const scrambleInterval = setInterval(() => {
if (!stopped) display.textContent = randomCode();
}, 80);
const countdownInterval = setInterval(() => {
if (stopped) return;
remaining--;
const m = Math.floor(remaining / 60);
const s = remaining % 60;
countdown.textContent = `Weiterleitung in ${m}:${String(s).padStart(2,'0')}`;
if (remaining <= 0) finish();
}, 1000);
}
</script>
</body>
</html>