Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
926 lines
44 KiB
HTML
926 lines
44 KiB
HTML
<!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">
|
||
<label for="testLock">Test-Lock <span class="form-hint">(kein echter Lock, zum Ausprobieren)</span></label>
|
||
</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('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 (4–9 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' });
|
||
}
|
||
|
||
// ── 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;
|
||
|
||
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,
|
||
};
|
||
} 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,
|
||
};
|
||
}
|
||
|
||
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>
|