Weiter am Ingame Chastity Game gearbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-26 22:53:05 +02:00
parent 4f2048bdc8
commit 0aa794600e
44 changed files with 2156 additions and 1419 deletions

View File

@@ -268,47 +268,6 @@
.sim-stat-val { font-size:1rem; font-weight:700; color:var(--color-text); }
.sim-stat-lbl { font-size:0.7rem; color:var(--color-muted); margin-top:0.15rem; text-transform:uppercase; letter-spacing:0.05em; }
/* ── Spiel-Sets (gruppe-card style) ── */
.gs-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; overflow:hidden; cursor:pointer; transition:border-color 0.15s; }
.gs-card:hover { border-color:var(--color-primary); }
.gs-card-header { display:flex; align-items:center; gap:0.75rem; padding:0.85rem 1rem; user-select:none; }
.gs-card-meta { flex:1; min-width:0; }
.gs-card-name { font-size:0.95rem; font-weight:600; color:var(--color-text); }
.gs-card-header-actions { display:flex; gap:0.4rem; flex-shrink:0; }
.gs-sub { margin-bottom:0.85rem; }
.gs-sub:last-child { margin-bottom:0; }
.gs-sub-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:0.35rem; }
.gs-sub-title { font-size:0.7rem; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; color:var(--color-primary); }
.gs-sub-warn { color:#e74c3c !important; }
.gs-item-list { display:flex; flex-direction:column; gap:0.25rem; }
.gs-list-item { border-radius:6px; background:var(--color-secondary); overflow:hidden; }
.gs-list-item-row { display:flex; align-items:center; gap:0.5rem; padding:0.3rem 0.5rem 0.3rem 0.75rem; cursor:pointer; user-select:none; transition:background 0.12s; }
.gs-list-item-row:hover { background:rgba(255,255,255,0.04); }
.gs-list-item.open .gs-list-item-row { background:rgba(233,69,96,0.08); }
.gs-list-item-text { color:var(--color-text); flex:1; min-width:0; font-size:0.83rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.gs-list-item-badges { display:flex; gap:0.3rem; flex-shrink:0; align-items:center; }
.gs-badge { font-size:0.68rem; padding:0.1rem 0.4rem; border-radius:20px; background:rgba(233,69,96,0.15); color:var(--color-primary); white-space:nowrap; }
.gs-badge-neutral { background:rgba(255,255,255,0.07); color:var(--color-muted); }
.gs-list-item-detail { display:none; padding:0.4rem 0.75rem 0.5rem; border-top:1px solid rgba(255,255,255,0.06); font-size:0.79rem; color:var(--color-muted); line-height:1.5; }
.gs-list-item.open .gs-list-item-detail { display:block; }
.gs-detail-text { color:var(--color-text); white-space:pre-wrap; margin-bottom:0.3rem; }
.gs-detail-actions { display:flex; gap:0.35rem; margin-top:0.4rem; justify-content:flex-end; }
.gs-btn-sub-add { background:none; border:1px solid var(--color-secondary); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.15rem 0.5rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
.gs-btn-sub-add:hover { border-color:var(--color-primary); color:var(--color-primary); }
.gs-btn-item-edit { background:none; border:1px solid rgba(136,136,136,0.45); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
.gs-btn-item-edit:hover { border-color:var(--color-text); color:var(--color-text); }
.gs-btn-item-delete { background:none; border:1px solid rgba(233,69,96,0.4); border-radius:5px; color:var(--color-primary); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:background 0.15s; width:auto; }
.gs-btn-item-delete:hover { background:rgba(233,69,96,0.15); }
.gs-sub-empty { font-size:0.78rem; color:var(--color-muted); padding:0.15rem 0; }
#gsSetModal, #gsItemModal { z-index:600; }
.gs-check-group { display:flex; flex-wrap:wrap; gap:0.4rem; }
.gs-check-chip { display:inline-flex; align-items:center; gap:0.4rem; background:var(--color-secondary); border:1px solid rgba(255,255,255,0.1); border-radius:20px; padding:0.25rem 0.7rem; cursor:pointer; font-size:0.82rem; color:var(--color-text); transition:border-color 0.15s; user-select:none; }
.gs-check-chip:has(input:checked) { border-color:var(--color-primary); }
.gs-check-chip input { accent-color:var(--color-primary); width:auto; cursor:pointer; margin:0; flex-shrink:0; }
</style>
</head>
<body class="app">
@@ -329,17 +288,6 @@
<div class="template-list" id="taskSetList"></div>
<p id="taskSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Aufgaben-Sets vorhanden.</p>
<!-- Spiel-Sets (für Spiel-Karte) -->
<div style="display:flex;align-items:center;justify-content:space-between;margin:2rem 0 1rem;gap:1rem;flex-wrap:wrap;">
<div>
<h2 style="margin:0;">Spiel-Sets</h2>
<p style="margin:0.25rem 0 0;font-size:0.8rem;color:var(--color-muted);">Aufgaben-Sets für die Spiel-Karte im Karten-Lock · max. 5 Sets</p>
</div>
<button id="btnNewGameSet" onclick="openGsSetModal(null)" style="width:auto;padding:0.55rem 1.2rem;">+ Set anlegen</button>
</div>
<div class="template-list" id="gameSetList"></div>
<p id="gameSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Spiel-Sets vorhanden.</p>
<h2 style="margin:2rem 0 1rem;">Abonnierte Vorlagen</h2>
<div class="template-list" id="subscribedList"></div>
<p id="subscribedEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Keine abonnierten Vorlagen vorhanden.</p>
@@ -443,24 +391,6 @@
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
</div>
</div>
<!-- Spiel-Karte (CardLock) -->
<div class="form-section">
<div class="form-section-title">Spiel-Karte (optional)</div>
<div class="form-row" style="margin-bottom:0.5rem;">
<label>Spiel-Set</label>
<select id="fGameSetId" onchange="onGameSetChange()">
<option value="">Kein Spiel-Set</option>
</select>
</div>
<button class="btn-add" type="button" onclick="openGsSetModal(null,'template')">+ Neues Set anlegen</button>
<div id="gameSetSpieldauerRow" style="display:none;margin-top:0.75rem;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.3rem;">
<label for="sldGameSpieldauer" style="font-size:0.88rem;">Spieldauer</label>
<span class="setting-value" id="valGameSpieldauer">Mittel</span>
</div>
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2" oninput="updateGameSpieldauer(this.value)" style="width:100%;">
</div>
</div>
<!-- Aufgaben (CardLock) -->
<div class="form-section">
<div class="form-section-title">Aufgaben (optional)</div>
@@ -719,114 +649,6 @@
</div>
</div>
<!-- Spiel-Set: Set erstellen / umbenennen -->
<div class="modal-backdrop" id="gsSetModal">
<div class="modal-box" style="max-width:420px;" onclick="event.stopPropagation()">
<h2 id="gsSetModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Neues Spiel-Set</h2>
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Name *</label>
<input type="text" id="gsSetName" maxlength="100" placeholder="Set-Name"
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;transition:border-color 0.2s;"
onkeydown="if(event.key==='Enter')saveGsSet()">
<div id="gsSetError" style="color:#e74c3c;font-size:0.82rem;margin-top:0.75rem;display:none;"></div>
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:1.5rem;">
<button onclick="closeGsSetModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
<button onclick="saveGsSet()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
</div>
</div>
</div>
<!-- Spiel-Set: Item hinzufügen / bearbeiten -->
<div class="modal-backdrop" id="gsItemModal">
<div class="modal-box" style="max-width:460px;" onclick="event.stopPropagation()">
<h2 id="gsItemModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Aufgabe</h2>
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Titel *</label>
<input type="text" id="gsItemTitle" maxlength="150" placeholder="Titel …"
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;margin-bottom:0.85rem;transition:border-color 0.2s;">
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Beschreibung (optional)</label>
<textarea id="gsItemDesc" rows="3" maxlength="600" placeholder="Beschreibung …"
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;margin-bottom:0.85rem;"></textarea>
<!-- Aufgabe: Level + Dauer -->
<div id="gsItemAufgabeRow" style="display:none;margin-bottom:0.85rem;">
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
<select id="gsItemAufgabeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
</select>
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Dauer (Minuten, optional)</label>
<input type="number" id="gsItemMinutes" min="1" max="9999" placeholder=""
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
</div>
<!-- Aufgabe: benötigt -->
<div id="gsItemBenoetigtRow" style="display:none;margin-bottom:0.85rem;">
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Benötigt</div>
<div class="gs-check-group">
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_MUND">Mund</label>
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_ANUS">Anus</label>
</div>
</div>
<!-- Zeitstrafe: Level + Strafminuten -->
<div id="gsItemZeitstrafeRow" style="display:none;margin-bottom:0.85rem;">
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
<select id="gsItemZeitstrafeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
</select>
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Strafminuten (Von Bis)</label>
<div style="display:flex;gap:0.6rem;align-items:center;">
<input type="number" id="gsItemMinMin" min="1" max="9999" placeholder="Min."
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
<span style="color:var(--color-muted);"></span>
<input type="number" id="gsItemMaxMin" min="1" max="9999" placeholder="Max."
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
</div>
<label style="display:block;font-size:0.8rem;color:#aaa;margin:0.65rem 0 0.3rem;">Text bei Aufhebung (optional)</label>
<textarea id="gsItemReleaseText" rows="2" maxlength="2000"
placeholder="Text der angezeigt wird, wenn die Sperre endet…"
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;transition:border-color 0.2s;line-height:1.45;"></textarea>
</div>
<!-- Zeitstrafe: sperrt -->
<div id="gsItemSperrtRow" style="display:none;margin-bottom:0.85rem;">
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Sperrt</div>
<div class="gs-check-group">
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_MUND"> Mund</label>
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_ANUS"> Anus</label>
</div>
</div>
<!-- Zeitstrafe + Finisher: Temp. Öffnung -->
<div id="gsItemUnlockRow" style="display:none;margin-bottom:0.85rem;">
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Temp. Öffnung</div>
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;margin-bottom:0.35rem;cursor:pointer;">
<input type="checkbox" id="gsItemBefore" style="accent-color:var(--color-primary);width:15px;height:15px;"> Vor der Maßnahme erforderlich
</label>
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
<input type="checkbox" id="gsItemAfter" style="accent-color:var(--color-primary);width:15px;height:15px;"> Nach der Maßnahme erforderlich
</label>
</div>
<div id="gsItemError" style="color:#e74c3c;font-size:0.82rem;display:none;margin-bottom:0.5rem;"></div>
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:0.5rem;">
<button onclick="closeGsItemModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
<button onclick="saveGsItem()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
</div>
</div>
</div>
<!-- Spiel-Set: Inhalt-Popup -->
<div class="modal-backdrop" id="gsEditModal">
<div class="modal-box" style="max-width:600px;width:calc(100% - 2rem);" onclick="event.stopPropagation()">
<div style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;margin-bottom:1.25rem;">
<h2 id="gsEditModalTitle" style="margin:0;color:var(--color-primary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></h2>
<button onclick="closeGsEditModal()" style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;line-height:1;cursor:pointer;padding:0;width:auto;flex-shrink:0;"></button>
</div>
<div id="gsEditModalContent"></div>
</div>
</div>
<script src="/js/card-defs.js"></script>
<script src="/js/card-display.js"></script>
<script src="/js/icons.js"></script>
@@ -1223,51 +1045,6 @@
if (res.ok || res.status === 204) await loadTaskSets();
}
const GS_TOOLS = [
{ value: 'UMSCHNALLDILDO', label: 'Strap-on' },
{ value: 'MUND', label: 'Oral' },
{ value: 'ANUS', label: 'Anal' },
];
function gsGetChecked(prefix) {
return GS_TOOLS.filter(t => document.getElementById(prefix + t.value)?.checked).map(t => t.value);
}
function gsSetChecked(prefix, values) {
GS_TOOLS.forEach(t => {
const el = document.getElementById(prefix + t.value);
if (el) el.checked = (values || []).includes(t.value);
});
}
const GAME_SPIELDAUER = [
{ label: 'Sehr kurz' },
{ label: 'Kurz' },
{ label: 'Mittel' },
{ label: 'Lang' },
{ label: 'Sehr lang' },
];
function populateGameSetSelect() {
const sel = document.getElementById('fGameSetId');
if (!sel) return;
const cur = sel.value;
sel.innerHTML = '<option value="">Kein Spiel-Set</option>';
_gameSets.forEach(s => {
const opt = document.createElement('option');
opt.value = s.id; opt.textContent = s.name;
sel.appendChild(opt);
});
sel.value = cur;
}
function onGameSetChange() {
const val = document.getElementById('fGameSetId')?.value;
document.getElementById('gameSetSpieldauerRow').style.display = val ? '' : 'none';
}
function updateGameSpieldauer(val) {
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[val]?.label || '';
}
function populateTaskSetSelects() {
for (const selId of ['fCardTaskSetId', 'fTimelockTaskSetId']) {
const sel = document.getElementById(selId);
@@ -1476,14 +1253,6 @@
onTaskSetChange('card');
onTaskSetChange('timelock');
// Spiel-Set
populateGameSetSelect();
document.getElementById('fGameSetId').value = template?.gameSetId || '';
onGameSetChange();
const sdIdx = template?.gameSpieldauerIdx ?? 2;
document.getElementById('sldGameSpieldauer').value = sdIdx;
updateGameSpieldauer(sdIdx);
alignModalToContent();
document.getElementById('modalBackdrop').classList.add('open');
document.getElementById('modalDiscardConfirm').style.display = 'none';
@@ -1527,13 +1296,7 @@
document.getElementById('modalBackdrop').addEventListener('click', e => { if (e.target===e.currentTarget) tryCloseModal(); });
document.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
if (document.getElementById('gsItemModal').classList.contains('open')) {
e.preventDefault(); closeGsItemModal();
} else if (document.getElementById('gsSetModal').classList.contains('open')) {
e.preventDefault(); closeGsSetModal();
} else if (document.getElementById('gsEditModal').classList.contains('open')) {
e.preventDefault(); closeGsEditModal();
} else if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) {
if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) {
e.preventDefault(); tryCloseTaskSetModal();
} else if (document.getElementById('modalBackdrop').classList.contains('open')) {
e.preventDefault(); tryCloseModal();
@@ -1588,9 +1351,6 @@
if (totalMax===0) { showModalError('Das Deck muss mindestens eine Karte enthalten.'); firstError=firstError||document.getElementById('modalError'); }
const hasTaskCards = (parseInt(document.getElementById('min_TASK').value)||0)>0 || (parseInt(document.getElementById('max_TASK').value)||0)>0;
if (hasTaskCards && !document.getElementById('fCardTaskSetId').value) { showModalError('Aufgaben-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
const hasGameCards = (parseInt(document.getElementById('min_GAME_CARD').value)||0)>0 || (parseInt(document.getElementById('max_GAME_CARD').value)||0)>0;
if (hasGameCards && !document.getElementById('fGameSetId').value) { showModalError('Spiel-Karten konfiguriert, aber kein Spiel-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
const cardCountsMin={}, cardCountsMax={};
@@ -1609,8 +1369,6 @@
taskSetId: document.getElementById('fCardTaskSetId').value || null,
requiresVerification: document.getElementById('fRequiresVerification').checked,
taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM',
gameSetId: document.getElementById('fGameSetId').value || null,
gameSpieldauerIdx: parseInt(document.getElementById('sldGameSpieldauer').value) || 2,
};
} else {
// TimeLock
@@ -1895,426 +1653,8 @@
observer.observe(document.getElementById('scrollSentinel'));
resetList();
loadGameSets();
// ════════════════════════════════════════════════
// Spiel-Sets
// ════════════════════════════════════════════════
let _gameSets = [];
let _gsEditSetId = null; // set being renamed
let _gsSetCaller = null; // 'template' when opened from the template modal
let _gsOpenSetId = null; // set currently open in the content popup
let _gsItemType = null; // 'aufgabe' | 'zeitstrafe' | 'finisher'
let _gsItemSetId = null;
let _gsItemIdx = null; // null = new, number = editing
async function loadGameSets() {
try {
const res = await fetch('/chastity/game-sets');
if (!res.ok) return;
_gameSets = await res.json();
renderGameSetList();
populateGameSetSelect();
if (_gsOpenSetId) renderGsEditModalContent(_gsOpenSetId);
} catch (e) { console.error(e); }
}
function renderGameSetList() {
const list = document.getElementById('gameSetList');
list.innerHTML = '';
document.getElementById('gameSetEmpty').style.display = _gameSets.length ? 'none' : '';
document.getElementById('btnNewGameSet').disabled = _gameSets.length >= 5;
_gameSets.forEach(s => {
const aufgaben = s.aufgaben || [];
const zeitstrafen = s.zeitstrafen || [];
const finisher = s.finisher || [];
const levelCounts = [1,2,3,4,5].map(l => aufgaben.filter(a => a.level === l).length);
const lvlBadges = levelCounts.map((c, i) => {
const cls = c >= 3 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
return `<span class="${cls}">L${i+1}: ${c}</span>`;
}).join('');
const finBadgeCls = finisher.length >= 1 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
const card = document.createElement('div');
card.className = 'gs-card';
card.id = 'gscard_' + s.id;
card.addEventListener('click', () => openGsEditModal(s.id));
card.innerHTML = `
<div class="gs-card-header">
<div class="gs-card-meta">
<div class="gs-card-name">${esc(s.name)}</div>
<div style="display:flex;gap:0.3rem;flex-wrap:wrap;margin-top:0.3rem;">
${lvlBadges}
<span class="gs-badge gs-badge-neutral">Zeitstrafen: ${zeitstrafen.length}</span>
<span class="${finBadgeCls}">Finisher: ${finisher.length}</span>
</div>
</div>
<div class="gs-card-header-actions" onclick="event.stopPropagation()">
<button class="gs-btn-item-edit" onclick="openGsSetModal('${s.id}')">✎</button>
<button class="gs-btn-item-delete" onclick="deleteGameSet('${s.id}',${JSON.stringify(s.name)})">✕</button>
</div>
</div>`;
list.appendChild(card);
});
}
function toggleGsListItem(id) {
document.getElementById(id)?.classList.toggle('open');
}
// ── Set content popup ──────────────────────────
function openGsEditModal(setId) {
_gsOpenSetId = setId;
renderGsEditModalContent(setId);
document.getElementById('gsEditModal').classList.add('open');
}
function closeGsEditModal() {
document.getElementById('gsEditModal').classList.remove('open');
_gsOpenSetId = null;
}
function renderGsEditModalContent(setId) {
const container = document.getElementById('gsEditModalContent');
if (!container) return;
const s = _gameSets.find(x => x.id === setId);
if (!s) { closeGsEditModal(); return; }
document.getElementById('gsEditModalTitle').textContent = s.name;
const aufgaben = s.aufgaben || [];
const zeitstrafen = s.zeitstrafen || [];
const finisher = s.finisher || [];
let html = '';
for (let l = 1; l <= 5; l++) {
const items = aufgaben.map((a, i) => ({...a, _gi: i})).filter(a => a.level === l);
const warnCls = items.length < 3 ? ' gs-sub-warn' : '';
const itemsHtml = items.map(a => gsAufgabeRowHtml(s.id, a._gi, a)).join('') ||
'<div class="gs-sub-empty"></div>';
html += `<div class="gs-sub">
<div class="gs-sub-header">
<span class="gs-sub-title${warnCls}">Level ${l} <span style="font-weight:400;">(${items.length}/3+)</span></span>
<button class="gs-btn-sub-add" onclick="openGsItemModal('aufgabe','${s.id}',null,${l})">+ Aufgabe</button>
</div>
<div class="gs-item-list">${itemsHtml}</div></div>`;
}
const zeitHtml = zeitstrafen.map((z, i) => gsZeitstrafeRowHtml(s.id, i, z)).join('') ||
'<div class="gs-sub-empty"></div>';
html += `<div class="gs-sub">
<div class="gs-sub-header">
<span class="gs-sub-title">Zeitstrafen <span style="font-weight:400;">(${zeitstrafen.length})</span></span>
<button class="gs-btn-sub-add" onclick="openGsItemModal('zeitstrafe','${s.id}',null,null)">+ Zeitstrafe</button>
</div>
<div class="gs-item-list">${zeitHtml}</div></div>`;
const finWarnCls = finisher.length < 1 ? ' gs-sub-warn' : '';
const finHtml = finisher.map((f, i) => gsFinisherRowHtml(s.id, i, f)).join('') ||
'<div class="gs-sub-empty"></div>';
html += `<div class="gs-sub">
<div class="gs-sub-header">
<span class="gs-sub-title${finWarnCls}">Finisher <span style="font-weight:400;">(${finisher.length}/1+)</span></span>
<button class="gs-btn-sub-add" onclick="openGsItemModal('finisher','${s.id}',null,null)">+ Finisher</button>
</div>
<div class="gs-item-list">${finHtml}</div></div>`;
container.innerHTML = html;
}
// ── Row HTML helpers ───────────────────────────
function gsAufgabeRowHtml(setId, gi, a) {
const toolLabels = (a.benoetigt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
const badges = [
a.minutes ? `<span class="gs-badge gs-badge-neutral">${a.minutes} Min.</span>` : '',
...toolLabels.map(l => `<span class="gs-badge gs-badge-neutral">${l}</span>`),
].join('');
const desc = a.description ? `<div class="gs-detail-text">${esc(a.description)}</div>` : '';
return `<div class="gs-list-item" id="gsli_${setId}_a_${gi}">
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_a_${gi}')">
<span class="gs-list-item-text">${esc(a.title)}</span>
<div class="gs-list-item-badges">${badges}</div>
</div>
<div class="gs-list-item-detail">${desc}
<div class="gs-detail-actions">
<button class="gs-btn-item-edit" onclick="openGsItemModal('aufgabe','${setId}',${gi},null)">✎ Bearbeiten</button>
<button class="gs-btn-item-edit" onclick="duplicateGsItem('aufgabe','${setId}',${gi})">⧉ Kopie</button>
<button class="gs-btn-item-delete" onclick="deleteGsItem('aufgabe','${setId}',${gi})">✕ Löschen</button>
</div>
</div></div>`;
}
function gsZeitstrafeRowHtml(setId, idx, z) {
const timeStr = (z.minMinutes != null ? z.minMinutes : '?') + '' + (z.maxMinutes != null ? z.maxMinutes : '?') + ' Min.';
const sperrtLabels = (z.sperrt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
const badges = [
z.level ? `<span class="gs-badge">L${z.level}</span>` : '',
`<span class="gs-badge gs-badge-neutral">${timeStr}</span>`,
...sperrtLabels.map(l => `<span class="gs-badge gs-badge-neutral">🔒 ${l}</span>`),
z.releaseText ? `<span class="gs-badge gs-badge-neutral">📝 Aufhebung</span>` : '',
z.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
z.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
].join('');
const releaseRow = z.releaseText ? `<div style="font-size:0.75rem;color:var(--color-muted);margin-bottom:0.15rem;">Bei Aufhebung:</div><div class="gs-detail-text">${esc(z.releaseText)}</div>` : '';
const desc = z.description ? `<div class="gs-detail-text">${esc(z.description)}</div>` : '';
return `<div class="gs-list-item" id="gsli_${setId}_z_${idx}">
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_z_${idx}')">
<span class="gs-list-item-text">${esc(z.title)}</span>
<div class="gs-list-item-badges">${badges}</div>
</div>
<div class="gs-list-item-detail">${desc}${releaseRow}
<div class="gs-detail-actions">
<button class="gs-btn-item-edit" onclick="openGsItemModal('zeitstrafe','${setId}',${idx},null)">✎ Bearbeiten</button>
<button class="gs-btn-item-edit" onclick="duplicateGsItem('zeitstrafe','${setId}',${idx})">⧉ Kopie</button>
<button class="gs-btn-item-delete" onclick="deleteGsItem('zeitstrafe','${setId}',${idx})">✕ Löschen</button>
</div>
</div></div>`;
}
function gsFinisherRowHtml(setId, idx, f) {
const badges = [
f.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
f.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
].join('');
const desc = f.description ? `<div class="gs-detail-text">${esc(f.description)}</div>` : '';
return `<div class="gs-list-item" id="gsli_${setId}_f_${idx}">
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_f_${idx}')">
<span class="gs-list-item-text">${esc(f.title)}</span>
<div class="gs-list-item-badges">${badges}</div>
</div>
<div class="gs-list-item-detail">${desc}
<div class="gs-detail-actions">
<button class="gs-btn-item-edit" onclick="openGsItemModal('finisher','${setId}',${idx},null)">✎ Bearbeiten</button>
<button class="gs-btn-item-edit" onclick="duplicateGsItem('finisher','${setId}',${idx})">⧉ Kopie</button>
<button class="gs-btn-item-delete" onclick="deleteGsItem('finisher','${setId}',${idx})">✕ Löschen</button>
</div>
</div></div>`;
}
// ── Set create / rename modal ──────────────────
function openGsSetModal(id, caller) {
_gsEditSetId = id || null;
_gsSetCaller = caller || null;
document.getElementById('gsSetModalTitle').textContent = id ? 'Spiel-Set umbenennen' : 'Neues Spiel-Set';
document.getElementById('gsSetName').value = id ? (_gameSets.find(s => s.id === id)?.name || '') : '';
document.getElementById('gsSetError').style.display = 'none';
document.getElementById('gsSetModal').classList.add('open');
setTimeout(() => document.getElementById('gsSetName').focus(), 50);
}
function closeGsSetModal() {
document.getElementById('gsSetModal').classList.remove('open');
_gsEditSetId = _gsSetCaller = null;
}
async function saveGsSet() {
const name = document.getElementById('gsSetName').value.trim();
const errEl = document.getElementById('gsSetError');
if (!name) { errEl.textContent = 'Name ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
errEl.style.display = 'none';
const set = _gsEditSetId ? _gameSets.find(s => s.id === _gsEditSetId) : null;
const url = _gsEditSetId ? `/chastity/game-sets/${_gsEditSetId}` : '/chastity/game-sets';
const method = _gsEditSetId ? 'PUT' : 'POST';
const body = { name,
aufgaben: set?.aufgaben || [],
zeitstrafen: set?.zeitstrafen || [],
finisher: set?.finisher || [] };
try {
const res = await fetch(url, { method, headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
if (res.ok) {
const saved = await res.json().catch(() => null);
const caller = _gsSetCaller;
closeGsSetModal();
await loadGameSets();
if (caller === 'template' && saved?.id) {
document.getElementById('fGameSetId').value = saved.id;
onGameSetChange();
markDirty();
}
return;
}
const b = await res.json().catch(() => ({}));
errEl.textContent = b.error || 'Fehler.'; errEl.style.display = '';
} catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
}
async function deleteGameSet(id, name) {
if (!confirm(`Spiel-Set „${name}" wirklich löschen?`)) return;
const res = await fetch(`/chastity/game-sets/${id}`, { method: 'DELETE' });
if (res.ok || res.status === 204) loadGameSets();
}
// ── Item modal ─────────────────────────────────
function openGsItemModal(type, setId, itemIdx, contextLevel) {
_gsItemType = type;
_gsItemSetId = setId;
_gsItemIdx = itemIdx !== null && itemIdx !== undefined ? itemIdx : null;
const titles = { aufgabe: 'Aufgabe', zeitstrafe: 'Zeitstrafe', finisher: 'Finisher' };
document.getElementById('gsItemModalTitle').textContent =
(_gsItemIdx !== null ? 'Bearbeiten: ' : 'Neu: ') + titles[type];
// Reset fields
document.getElementById('gsItemTitle').value = '';
document.getElementById('gsItemDesc').value = '';
document.getElementById('gsItemMinutes').value = '';
document.getElementById('gsItemMinMin').value = '';
document.getElementById('gsItemMaxMin').value = '';
document.getElementById('gsItemReleaseText').value = '';
document.getElementById('gsItemBefore').checked = false;
document.getElementById('gsItemAfter').checked = false;
document.getElementById('gsItemAufgabeLevel').value = contextLevel || 1;
document.getElementById('gsItemZeitstrafeLevel').value = 1;
document.getElementById('gsItemError').style.display = 'none';
gsSetChecked('gsItemBen_', []);
gsSetChecked('gsItemSperr_', []);
// Show/hide type-specific rows
document.getElementById('gsItemAufgabeRow').style.display = type === 'aufgabe' ? '' : 'none';
document.getElementById('gsItemBenoetigtRow').style.display = type === 'aufgabe' ? '' : 'none';
document.getElementById('gsItemZeitstrafeRow').style.display = type === 'zeitstrafe' ? '' : 'none';
document.getElementById('gsItemSperrtRow').style.display = type === 'zeitstrafe' ? '' : 'none';
document.getElementById('gsItemUnlockRow').style.display = (type === 'zeitstrafe' || type === 'finisher') ? '' : 'none';
// Pre-fill when editing
if (_gsItemIdx !== null) {
const set = _gameSets.find(s => s.id === setId);
if (set) {
let item;
if (type === 'aufgabe') item = set.aufgaben[_gsItemIdx];
if (type === 'zeitstrafe') item = set.zeitstrafen[_gsItemIdx];
if (type === 'finisher') item = set.finisher[_gsItemIdx];
if (item) {
document.getElementById('gsItemTitle').value = item.title || '';
document.getElementById('gsItemDesc').value = item.description || '';
if (type === 'aufgabe') {
document.getElementById('gsItemAufgabeLevel').value = item.level || 1;
document.getElementById('gsItemMinutes').value = item.minutes || '';
gsSetChecked('gsItemBen_', item.benoetigt || []);
}
if (type === 'zeitstrafe') {
document.getElementById('gsItemZeitstrafeLevel').value = item.level || 1;
document.getElementById('gsItemMinMin').value = item.minMinutes ?? '';
document.getElementById('gsItemMaxMin').value = item.maxMinutes ?? '';
document.getElementById('gsItemReleaseText').value = item.releaseText || '';
gsSetChecked('gsItemSperr_', item.sperrt || []);
}
if (type === 'zeitstrafe' || type === 'finisher') {
document.getElementById('gsItemBefore').checked = !!item.tempUnlockBeforeRequired;
document.getElementById('gsItemAfter').checked = !!item.tempUnlockAfterRequired;
}
}
}
}
document.getElementById('gsItemModal').classList.add('open');
setTimeout(() => document.getElementById('gsItemTitle').focus(), 50);
}
function closeGsItemModal() {
document.getElementById('gsItemModal').classList.remove('open');
_gsItemType = _gsItemSetId = _gsItemIdx = null;
}
async function saveGsItem() {
const title = document.getElementById('gsItemTitle').value.trim();
const errEl = document.getElementById('gsItemError');
if (!title) { errEl.textContent = 'Titel ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
errEl.style.display = 'none';
const set = _gameSets.find(s => s.id === _gsItemSetId);
if (!set) return;
const updated = {
name: set.name,
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
};
const desc = document.getElementById('gsItemDesc').value.trim() || null;
let item;
if (_gsItemType === 'aufgabe') {
const min = parseInt(document.getElementById('gsItemMinutes').value);
const ben = gsGetChecked('gsItemBen_');
item = { title, description: desc,
level: parseInt(document.getElementById('gsItemAufgabeLevel').value) || 1,
minutes: isNaN(min) ? null : min,
benoetigt: ben.length ? ben : null };
if (_gsItemIdx !== null) updated.aufgaben[_gsItemIdx] = item;
else updated.aufgaben.push(item);
} else if (_gsItemType === 'zeitstrafe') {
const minMin = parseInt(document.getElementById('gsItemMinMin').value);
const maxMin = parseInt(document.getElementById('gsItemMaxMin').value);
const sperrt = gsGetChecked('gsItemSperr_');
const releaseText = document.getElementById('gsItemReleaseText').value.trim() || null;
item = { title, description: desc,
level: parseInt(document.getElementById('gsItemZeitstrafeLevel').value) || 1,
minMinutes: isNaN(minMin) ? null : minMin,
maxMinutes: isNaN(maxMin) ? null : maxMin,
releaseText,
tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked,
sperrt: sperrt.length ? sperrt : null };
if (_gsItemIdx !== null) updated.zeitstrafen[_gsItemIdx] = item;
else updated.zeitstrafen.push(item);
} else if (_gsItemType === 'finisher') {
item = { title, description: desc,
tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked };
if (_gsItemIdx !== null) updated.finisher[_gsItemIdx] = item;
else updated.finisher.push(item);
}
try {
const res = await fetch(`/chastity/game-sets/${_gsItemSetId}`, {
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
});
if (res.ok) { closeGsItemModal(); await loadGameSets(); }
else { const b = await res.json().catch(()=>({})); errEl.textContent = b.error||'Fehler.'; errEl.style.display = ''; }
} catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
}
async function deleteGsItem(type, setId, idx) {
if (!confirm('Eintrag wirklich löschen?')) return;
const set = _gameSets.find(s => s.id === setId);
if (!set) return;
const updated = {
name: set.name,
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
};
if (type === 'aufgabe') updated.aufgaben.splice(idx, 1);
if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx, 1);
if (type === 'finisher') updated.finisher.splice(idx, 1);
const res = await fetch(`/chastity/game-sets/${setId}`, {
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
});
if (res.ok) loadGameSets();
}
async function duplicateGsItem(type, setId, idx) {
const set = _gameSets.find(s => s.id === setId);
if (!set) return;
const updated = {
name: set.name,
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
};
if (type === 'aufgabe') updated.aufgaben.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.aufgaben[idx])));
if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.zeitstrafen[idx])));
if (type === 'finisher') updated.finisher.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.finisher[idx])));
const res = await fetch(`/chastity/game-sets/${setId}`, {
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
});
if (res.ok) loadGameSets();
}
document.getElementById('gsSetModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsSetModal(); });
document.getElementById('gsItemModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsItemModal(); });
document.getElementById('gsEditModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsEditModal(); });
</script>
</body>
</html>