Weiter am Chastity Ingame gearbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
5
bin/main/db/migration/V4__lock_game_session_nullable.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- session_id war fälschlicherweise PRIMARY KEY der lock_game-Tabelle.
|
||||||
|
-- Korrekter PK ist game_id (gemäß JPA-Entity @Id).
|
||||||
|
ALTER TABLE lock_game DROP PRIMARY KEY;
|
||||||
|
ALTER TABLE lock_game MODIFY COLUMN session_id VARCHAR(255) NULL;
|
||||||
|
ALTER TABLE lock_game ADD PRIMARY KEY (game_id);
|
||||||
5
bin/main/db/migration/V5__lock_game_text_columns.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- task_in_queue und lock_in_queue können serialisierte JSON-Objekte speichern,
|
||||||
|
-- die länger als 255 Zeichen sind – auf TEXT erweitern.
|
||||||
|
ALTER TABLE lock_game
|
||||||
|
MODIFY COLUMN task_in_queue TEXT NULL,
|
||||||
|
MODIFY COLUMN lock_in_queue TEXT NULL;
|
||||||
5
bin/main/db/migration/V6__game_active_default.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- game_active wurde ohne DEFAULT angelegt; bestehende Zeilen haben NULL –
|
||||||
|
-- auf false setzen und NOT NULL + DEFAULT ergänzen.
|
||||||
|
UPDATE active_lock SET game_active = 0 WHERE game_active IS NULL;
|
||||||
|
ALTER TABLE active_lock
|
||||||
|
MODIFY COLUMN game_active TINYINT(1) NOT NULL DEFAULT 0;
|
||||||
18
bin/main/static/css/time-picker.css
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.time-picker { display:flex; align-items:center; gap:0.35rem; flex-wrap:wrap; }
|
||||||
|
.tp-seg { display:flex; flex-direction:column; align-items:center; gap:0; }
|
||||||
|
.tp-seg-row { display:flex; align-items:center; gap:0.2rem; }
|
||||||
|
.tp-seg button {
|
||||||
|
width:24px !important; height:24px !important;
|
||||||
|
background:var(--color-card); border:1px solid var(--color-muted);
|
||||||
|
border-radius:4px; cursor:pointer; font-size:0.9rem; font-weight:700; color:var(--color-text);
|
||||||
|
display:flex; align-items:center; justify-content:center; padding:0; flex-shrink:0;
|
||||||
|
}
|
||||||
|
.tp-seg button:hover { background:var(--color-primary); color:#fff; border-color:var(--color-primary); }
|
||||||
|
.tp-seg .tp-seg-row input {
|
||||||
|
width:28px !important; padding:0.15rem 0 !important;
|
||||||
|
text-align:center; background:var(--color-card);
|
||||||
|
border:1px solid var(--color-muted); border-radius:4px;
|
||||||
|
color:var(--color-text); font-size:0.9rem; font-weight:600;
|
||||||
|
font-family:monospace; box-sizing:border-box;
|
||||||
|
}
|
||||||
|
.tp-seg .tp-label { font-size:0.58rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.03em; line-height:1.2; }
|
||||||
@@ -532,6 +532,19 @@
|
|||||||
<label class="werkzeug-check"><span>Anus</span><input type="checkbox" value="ANUS"></label>
|
<label class="werkzeug-check"><span>Anus</span><input type="checkbox" value="ANUS"></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="iTempUnlockRow">
|
||||||
|
<label>Temporäre Öffnungen</label>
|
||||||
|
<div style="display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem;">
|
||||||
|
<label style="display:flex; align-items:center; gap:0.6rem; font-size:0.85rem; cursor:pointer;">
|
||||||
|
<input type="checkbox" id="iTempUnlockBefore" style="accent-color:var(--color-primary); width:1rem; height:1rem;">
|
||||||
|
Temporäre Öffnung <em>vor</em> der Zeitstrafe erforderlich
|
||||||
|
</label>
|
||||||
|
<label style="display:flex; align-items:center; gap:0.6rem; font-size:0.85rem; cursor:pointer;">
|
||||||
|
<input type="checkbox" id="iTempUnlockAfter" style="accent-color:var(--color-primary); width:1rem; height:1rem;">
|
||||||
|
Temporäre Öffnung <em>nach</em> der Zeitstrafe erforderlich
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="iReleaseTextRow">
|
<div id="iReleaseTextRow">
|
||||||
<label for="iReleaseText">Text bei Aufhebung</label>
|
<label for="iReleaseText">Text bei Aufhebung</label>
|
||||||
<textarea id="iReleaseText" rows="2" maxlength="2000" placeholder="Text der angezeigt wird, wenn die Sperre endet…"></textarea>
|
<textarea id="iReleaseText" rows="2" maxlength="2000" placeholder="Text der angezeigt wird, wenn die Sperre endet…"></textarea>
|
||||||
@@ -1411,13 +1424,14 @@
|
|||||||
document.getElementById('iWerkzeugFinisherPassivRow').style.display = (isFinisher && !isChastity) ? 'block' : 'none';
|
document.getElementById('iWerkzeugFinisherPassivRow').style.display = (isFinisher && !isChastity) ? 'block' : 'none';
|
||||||
|
|
||||||
// Zeitstrafe rows
|
// Zeitstrafe rows
|
||||||
document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none';
|
document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none';
|
||||||
document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none';
|
document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none';
|
||||||
['VAGINA', 'PENIS'].forEach(v => {
|
['VAGINA', 'PENIS'].forEach(v => {
|
||||||
const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label');
|
const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label');
|
||||||
if (lbl) lbl.style.display = isChastity ? 'none' : '';
|
if (lbl) lbl.style.display = isChastity ? 'none' : '';
|
||||||
});
|
});
|
||||||
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
document.getElementById('iTempUnlockRow').style.display = (isZeit && isChastity) ? 'block' : 'none';
|
||||||
|
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function _resetItemFields() {
|
function _resetItemFields() {
|
||||||
@@ -1435,6 +1449,8 @@
|
|||||||
document.querySelectorAll('#iWerkzeugFinisherPassiv input').forEach(cb => cb.checked = false);
|
document.querySelectorAll('#iWerkzeugFinisherPassiv input').forEach(cb => cb.checked = false);
|
||||||
document.querySelectorAll('#iSperreFuer input').forEach(cb => cb.checked = false);
|
document.querySelectorAll('#iSperreFuer input').forEach(cb => cb.checked = false);
|
||||||
document.querySelectorAll('#iGeschlecht input').forEach(rb => rb.checked = false);
|
document.querySelectorAll('#iGeschlecht input').forEach(rb => rb.checked = false);
|
||||||
|
document.getElementById('iTempUnlockBefore').checked = false;
|
||||||
|
document.getElementById('iTempUnlockAfter').checked = false;
|
||||||
_selectedToys = [];
|
_selectedToys = [];
|
||||||
renderSelectedToys();
|
renderSelectedToys();
|
||||||
document.getElementById('itemModalError').style.display = 'none';
|
document.getElementById('itemModalError').style.display = 'none';
|
||||||
@@ -1485,7 +1501,9 @@
|
|||||||
document.getElementById('iReleaseText').value = d.releaseText || '';
|
document.getElementById('iReleaseText').value = d.releaseText || '';
|
||||||
(d.sperreFuer || []).forEach(w => { const cb = document.querySelector(`#iSperreFuer input[value="${w}"]`); if (cb) cb.checked = true; });
|
(d.sperreFuer || []).forEach(w => { const cb = document.querySelector(`#iSperreFuer input[value="${w}"]`); if (cb) cb.checked = true; });
|
||||||
if (_isChastityMode) {
|
if (_isChastityMode) {
|
||||||
document.getElementById('iLevel').value = d.level != null ? d.level : '';
|
document.getElementById('iLevel').value = d.level != null ? d.level : '';
|
||||||
|
document.getElementById('iTempUnlockBefore').checked = d.tempUnlockBeforeRequired === true;
|
||||||
|
document.getElementById('iTempUnlockAfter').checked = d.tempUnlockAfterRequired === true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1680,6 +1698,8 @@
|
|||||||
releaseText: document.getElementById('iReleaseText').value.trim() || null,
|
releaseText: document.getElementById('iReleaseText').value.trim() || null,
|
||||||
sperreFuer,
|
sperreFuer,
|
||||||
level: zeitLevel,
|
level: zeitLevel,
|
||||||
|
tempUnlockBeforeRequired: _isChastityMode ? document.getElementById('iTempUnlockBefore').checked : null,
|
||||||
|
tempUnlockAfterRequired: _isChastityMode ? document.getElementById('iTempUnlockAfter').checked : null,
|
||||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||||
};
|
};
|
||||||
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only
|
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only
|
||||||
|
|||||||
@@ -658,6 +658,23 @@
|
|||||||
<button class="btn-hygiene" id="hygieneBtn" style="display:none;" onclick="openHygieneModal()">🚿 Hygiene-Öffnung</button>
|
<button class="btn-hygiene" id="hygieneBtn" style="display:none;" onclick="openHygieneModal()">🚿 Hygiene-Öffnung</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Geparkte Spiel-Karte -->
|
||||||
|
<div id="gameCardPanel" class="hygiene-panel" style="display:none;border-color:var(--color-primary);">
|
||||||
|
<div>
|
||||||
|
<div class="hygiene-panel-title" style="color:var(--color-primary);">🎮 Spiel-Karte gezogen</div>
|
||||||
|
<div class="hygiene-countdown" id="gameCardCountdown">–</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-hygiene" onclick="startParkedGame()">▶ Spiel starten</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gameActivePanel" class="hygiene-panel" style="display:none;border-color:var(--color-primary);">
|
||||||
|
<div>
|
||||||
|
<div class="hygiene-panel-title" style="color:var(--color-primary);">🎮 Minispiel läuft</div>
|
||||||
|
<div style="font-size:0.82rem;color:var(--color-muted);">Das Lock ist gesperrt, bis das Spiel abgeschlossen ist.</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-hygiene" onclick="goToActiveGame()">▶ Zum Spiel</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Speed-Effekt-Panel -->
|
<!-- Speed-Effekt-Panel -->
|
||||||
<div id="speedPanel" style="display:none;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:12px;padding:0.85rem 1.1rem;gap:0.35rem;">
|
<div id="speedPanel" style="display:none;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:12px;padding:0.85rem 1.1rem;gap:0.35rem;">
|
||||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--color-muted);" id="speedPanelTitle">Slow Motion aktiv</div>
|
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--color-muted);" id="speedPanelTitle">Slow Motion aktiv</div>
|
||||||
@@ -909,6 +926,8 @@
|
|||||||
renderKeyholderBar(lock);
|
renderKeyholderBar(lock);
|
||||||
renderAssignedTasks(lock);
|
renderAssignedTasks(lock);
|
||||||
renderNextCardPanel(lock);
|
renderNextCardPanel(lock);
|
||||||
|
renderGameCardPanel(lock);
|
||||||
|
renderGameActivePanel(lock);
|
||||||
renderHygienePanel(lock);
|
renderHygienePanel(lock);
|
||||||
renderSpeedPanel(lock);
|
renderSpeedPanel(lock);
|
||||||
renderVerificationPanel(lock);
|
renderVerificationPanel(lock);
|
||||||
@@ -1170,6 +1189,9 @@
|
|||||||
const panel = document.getElementById('nextcardPanel');
|
const panel = document.getElementById('nextcardPanel');
|
||||||
const cardsDiv = document.getElementById('nextcardCards');
|
const cardsDiv = document.getElementById('nextcardCards');
|
||||||
const overlay = document.getElementById('nextcardOverlay');
|
const overlay = document.getElementById('nextcardOverlay');
|
||||||
|
|
||||||
|
if (lock.gameActive) { panel.style.display = 'none'; return; }
|
||||||
|
|
||||||
panel.style.display = '';
|
panel.style.display = '';
|
||||||
panel.classList.remove('drawable');
|
panel.classList.remove('drawable');
|
||||||
|
|
||||||
@@ -1286,6 +1308,45 @@
|
|||||||
tickInterval = setInterval(tick, 1000);
|
tickInterval = setInterval(tick, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let gameCardPanelTick = null;
|
||||||
|
|
||||||
|
function renderGameCardPanel(lock) {
|
||||||
|
if (gameCardPanelTick) { clearInterval(gameCardPanelTick); gameCardPanelTick = null; }
|
||||||
|
const panel = document.getElementById('gameCardPanel');
|
||||||
|
if (!lock.gameCardParkedAt || !lock.gameCardDeadline) {
|
||||||
|
panel.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
panel.style.display = '';
|
||||||
|
const deadline = new Date(lock.gameCardDeadline);
|
||||||
|
const cdEl = document.getElementById('gameCardCountdown');
|
||||||
|
function tick() {
|
||||||
|
const diff = deadline - Date.now();
|
||||||
|
if (diff <= 0) { panel.style.display = 'none'; clearInterval(gameCardPanelTick); gameCardPanelTick = null; return; }
|
||||||
|
cdEl.textContent = fmtCountdown(diff);
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
gameCardPanelTick = setInterval(tick, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startParkedGame() {
|
||||||
|
const res = await fetch('/keyholder/cardlock/' + lockId + '/game/start', { method: 'POST' });
|
||||||
|
if (!res.ok) { alert('Fehler beim Starten des Spiels.'); return; }
|
||||||
|
const data = await res.json();
|
||||||
|
const gameSetId = data.gameSetId;
|
||||||
|
const url = '/games/chastity/taskgame.html?lockId=' + lockId
|
||||||
|
+ (gameSetId ? '&gameSetId=' + gameSetId : '');
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGameActivePanel(lock) {
|
||||||
|
document.getElementById('gameActivePanel').style.display = lock.gameActive ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToActiveGame() {
|
||||||
|
window.location.href = '/games/chastity/taskgame.html?lockId=' + lockId;
|
||||||
|
}
|
||||||
|
|
||||||
function renderHygienePanel(lock) {
|
function renderHygienePanel(lock) {
|
||||||
if (hygienePanelTick) { clearInterval(hygienePanelTick); hygienePanelTick = null; }
|
if (hygienePanelTick) { clearInterval(hygienePanelTick); hygienePanelTick = null; }
|
||||||
if (!lock.hygieneEnabled) return;
|
if (!lock.hygieneEnabled) return;
|
||||||
@@ -1714,10 +1775,8 @@
|
|||||||
|
|
||||||
if (dto.card === 'GAME_CARD') {
|
if (dto.card === 'GAME_CARD') {
|
||||||
const btn = document.getElementById('btnDrawOk');
|
const btn = document.getElementById('btnDrawOk');
|
||||||
btn.textContent = '▶ Spiel starten';
|
btn.textContent = 'OK';
|
||||||
btn.onclick = function() {
|
btn.onclick = closeDrawModal;
|
||||||
window.location.href = '/games/chastity/taskgame.html?lockId=' + lockId;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, 700);
|
}, 700);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>Keyholder – xXx Sphere</title>
|
<title>Keyholder – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/time-picker.css">
|
||||||
<style>
|
<style>
|
||||||
.lock-list { display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem; }
|
.lock-list { display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem; }
|
||||||
|
|
||||||
@@ -80,27 +81,6 @@
|
|||||||
.violation-item:last-child { border-bottom:none; }
|
.violation-item:last-child { border-bottom:none; }
|
||||||
|
|
||||||
/* Zeitpicker (für Freeze-Dialog) */
|
/* Zeitpicker (für Freeze-Dialog) */
|
||||||
.time-picker { display:flex; align-items:center; gap:0.5rem; }
|
|
||||||
.tp-seg { display:flex; flex-direction:column; align-items:center; gap:0.2rem; }
|
|
||||||
.tp-seg-row { display:flex; align-items:center; gap:0.25rem; }
|
|
||||||
.tp-seg button {
|
|
||||||
width:26px; height:26px;
|
|
||||||
background:var(--color-secondary); border:1px solid var(--color-muted);
|
|
||||||
border-radius:5px; cursor:pointer; font-size:1rem; font-weight:700;
|
|
||||||
color:var(--color-text); display:flex; align-items:center; justify-content:center;
|
|
||||||
padding:0; flex-shrink:0;
|
|
||||||
}
|
|
||||||
.tp-seg button:hover { background:var(--color-primary); color:#fff; border-color:var(--color-primary); }
|
|
||||||
.tp-seg input {
|
|
||||||
width:30px; text-align:center;
|
|
||||||
background:var(--color-secondary); border:1px solid var(--color-muted);
|
|
||||||
border-radius:4px; color:var(--color-text);
|
|
||||||
font-size:0.95rem; font-weight:600; font-family:monospace;
|
|
||||||
padding:0.18rem 0; box-sizing:border-box;
|
|
||||||
}
|
|
||||||
.tp-seg .tp-label { font-size:0.65rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; }
|
|
||||||
.tp-colon { font-size:1.1rem; font-weight:700; color:var(--color-muted); margin-bottom:1rem; }
|
|
||||||
|
|
||||||
/* ── Tab-Navigation ── */
|
/* ── Tab-Navigation ── */
|
||||||
.kh-tabs { display:flex; gap:0; border-bottom:2px solid var(--color-secondary); margin-bottom:1.25rem; }
|
.kh-tabs { display:flex; gap:0; border-bottom:2px solid var(--color-secondary); margin-bottom:1.25rem; }
|
||||||
.kh-tab {
|
.kh-tab {
|
||||||
@@ -311,32 +291,9 @@
|
|||||||
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.4rem;">Annahme-Frist</div>
|
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.4rem;">Annahme-Frist</div>
|
||||||
|
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('at',-1,'d')">−</button><input type="text" id="at_d" value="0" readonly><button type="button" onclick="tpChange('at',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-seg-row">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('at',-1,'h')">−</button><input type="text" id="at_h" value="01" readonly><button type="button" onclick="tpChange('at',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<button type="button" onclick="atTpChange(-1,'d')">−</button>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('at',-1,'m')">−</button><input type="text" id="at_m" value="00" readonly><button type="button" onclick="tpChange('at',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<input type="text" id="at_d" value="0" readonly>
|
|
||||||
<button type="button" onclick="atTpChange(1,'d')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Tage</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="atTpChange(-1,'h')">−</button>
|
|
||||||
<input type="text" id="at_h" value="01" readonly>
|
|
||||||
<button type="button" onclick="atTpChange(1,'h')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Stunden</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="atTpChange(-1,'m')">−</button>
|
|
||||||
<input type="text" id="at_m" value="00" readonly>
|
|
||||||
<button type="button" onclick="atTpChange(1,'m')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Minuten</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -350,32 +307,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<div id="atPenaltyFreezeFields" style="padding-left:1.6rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
|
<div id="atPenaltyFreezeFields" style="padding-left:1.6rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('atf',-1,'d')">−</button><input type="text" id="atf_d" value="0" readonly><button type="button" onclick="tpChange('atf',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-seg-row">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('atf',-1,'h')">−</button><input type="text" id="atf_h" value="04" readonly><button type="button" onclick="tpChange('atf',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<button type="button" onclick="atFreezeTpChange(-1,'d')">−</button>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('atf',-1,'m')">−</button><input type="text" id="atf_m" value="00" readonly><button type="button" onclick="tpChange('atf',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<input type="text" id="atf_d" value="0" readonly>
|
|
||||||
<button type="button" onclick="atFreezeTpChange(1,'d')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Tage</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="atFreezeTpChange(-1,'h')">−</button>
|
|
||||||
<input type="text" id="atf_h" value="04" readonly>
|
|
||||||
<button type="button" onclick="atFreezeTpChange(1,'h')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Stunden</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="atFreezeTpChange(-1,'m')">−</button>
|
|
||||||
<input type="text" id="atf_m" value="00" readonly>
|
|
||||||
<button type="button" onclick="atFreezeTpChange(1,'m')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Minuten</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
|
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
|
||||||
@@ -409,32 +343,9 @@
|
|||||||
<strong style="color:var(--color-text);">Hinweis:</strong> Während des Einfrierens können keine weiteren Karten gezogen werden.
|
<strong style="color:var(--color-text);">Hinweis:</strong> Während des Einfrierens können keine weiteren Karten gezogen werden.
|
||||||
</p>
|
</p>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('freeze',-1,'d')">−</button><input type="text" id="freeze_d" value="0" readonly><button type="button" onclick="tpChange('freeze',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-seg-row">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('freeze',-1,'h')">−</button><input type="text" id="freeze_h" value="04" readonly><button type="button" onclick="tpChange('freeze',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<button type="button" onclick="freezeTpChange(-1,'d')">−</button>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('freeze',-1,'m')">−</button><input type="text" id="freeze_m" value="00" readonly><button type="button" onclick="tpChange('freeze',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<input type="text" id="freeze_d" value="0" readonly>
|
|
||||||
<button type="button" onclick="freezeTpChange(1,'d')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Tage</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="freezeTpChange(-1,'h')">−</button>
|
|
||||||
<input type="text" id="freeze_h" value="04" readonly>
|
|
||||||
<button type="button" onclick="freezeTpChange(1,'h')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Stunden</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="freezeTpChange(-1,'m')">−</button>
|
|
||||||
<input type="text" id="freeze_m" value="00" readonly>
|
|
||||||
<button type="button" onclick="freezeTpChange(1,'m')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Minuten</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="freezeModalError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
|
<div id="freezeModalError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
|
||||||
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
|
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
|
||||||
@@ -462,8 +373,9 @@
|
|||||||
<script src="/js/card-defs.js"></script>
|
<script src="/js/card-defs.js"></script>
|
||||||
<script src="/js/card-display.js"></script>
|
<script src="/js/card-display.js"></script>
|
||||||
<script src="/js/icons.js"></script>
|
<script src="/js/icons.js"></script>
|
||||||
<script src="/js/nav.js"></script>
|
<script src="/js/nav.js"></script>
|
||||||
<script src="/js/social-sidebar.js"></script>
|
<script src="/js/social-sidebar.js"></script>
|
||||||
|
<script src="/js/time-picker.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||||
|
|
||||||
@@ -1095,41 +1007,6 @@
|
|||||||
let assignTaskLockId = null;
|
let assignTaskLockId = null;
|
||||||
let assignTaskSelectedIdx = null;
|
let assignTaskSelectedIdx = null;
|
||||||
|
|
||||||
function atTpChange(delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById('at_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById('at_h').value) || 0;
|
|
||||||
let m = parseInt(document.getElementById('at_m').value) || 0;
|
|
||||||
if (seg === 'm') m += delta; else if (seg === 'h') h += delta; else d += delta;
|
|
||||||
if (m >= 60) { h += Math.floor(m/60); m = m%60; }
|
|
||||||
if (m < 0) { const b = Math.ceil(-m/60); h -= b; m += b*60; }
|
|
||||||
if (h >= 24) { d += Math.floor(h/24); h = h%24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById('at_d').value = d;
|
|
||||||
document.getElementById('at_h').value = String(h).padStart(2,'0');
|
|
||||||
document.getElementById('at_m').value = String(m).padStart(2,'0');
|
|
||||||
}
|
|
||||||
|
|
||||||
function atFreezeTpChange(delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById('atf_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById('atf_h').value) || 0;
|
|
||||||
let m = parseInt(document.getElementById('atf_m').value) || 0;
|
|
||||||
if (seg === 'm') m += delta; else if (seg === 'h') h += delta; else d += delta;
|
|
||||||
if (m >= 60) { h += Math.floor(m/60); m = m%60; }
|
|
||||||
if (m < 0) { const b = Math.ceil(-m/60); h -= b; m += b*60; }
|
|
||||||
if (h >= 24) { d += Math.floor(h/24); h = h%24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById('atf_d').value = d;
|
|
||||||
document.getElementById('atf_h').value = String(h).padStart(2,'0');
|
|
||||||
document.getElementById('atf_m').value = String(m).padStart(2,'0');
|
|
||||||
}
|
|
||||||
|
|
||||||
function atTpToMinutes(prefix) {
|
|
||||||
return (parseInt(document.getElementById(prefix+'_d').value)||0) * 24*60
|
|
||||||
+ (parseInt(document.getElementById(prefix+'_h').value)||0) * 60
|
|
||||||
+ (parseInt(document.getElementById(prefix+'_m').value)||0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function atRedChange(delta) {
|
function atRedChange(delta) {
|
||||||
const inp = document.getElementById('atRedCount');
|
const inp = document.getElementById('atRedCount');
|
||||||
@@ -1273,7 +1150,7 @@
|
|||||||
errEl.style.display = '';
|
errEl.style.display = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const deadlineMinutes = atTpToMinutes('at');
|
const deadlineMinutes = tpToMinutes('at');
|
||||||
if (deadlineMinutes < 1) {
|
if (deadlineMinutes < 1) {
|
||||||
errEl.textContent = 'Bitte eine Annahme-Frist angeben.';
|
errEl.textContent = 'Bitte eine Annahme-Frist angeben.';
|
||||||
errEl.style.display = '';
|
errEl.style.display = '';
|
||||||
@@ -1286,7 +1163,7 @@
|
|||||||
errEl.style.display = '';
|
errEl.style.display = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const penaltyFreezeMinutes = freezeEnabled ? atTpToMinutes('atf') : null;
|
const penaltyFreezeMinutes = freezeEnabled ? tpToMinutes('atf') : null;
|
||||||
const penaltyRedCards = redEnabled ? parseInt(document.getElementById('atRedCount').value) || 1 : null;
|
const penaltyRedCards = redEnabled ? parseInt(document.getElementById('atRedCount').value) || 1 : null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1363,31 +1240,6 @@
|
|||||||
let freezeTargetLockId = null;
|
let freezeTargetLockId = null;
|
||||||
let unfreezeTargetLockId = null;
|
let unfreezeTargetLockId = null;
|
||||||
|
|
||||||
// Zeitpicker-Logik (Dauer)
|
|
||||||
function freezeTpChange(delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById('freeze_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById('freeze_h').value) || 0;
|
|
||||||
let m = parseInt(document.getElementById('freeze_m').value) || 0;
|
|
||||||
if (seg === 'm') m += delta;
|
|
||||||
else if (seg === 'h') h += delta;
|
|
||||||
else if (seg === 'd') d += delta;
|
|
||||||
if (m >= 60) { h += Math.floor(m / 60); m = m % 60; }
|
|
||||||
if (m < 0) { const b = Math.ceil(-m / 60); h -= b; m += b * 60; }
|
|
||||||
if (h >= 24) { d += Math.floor(h / 24); h = h % 24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h / 24); d -= b; h += b * 24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById('freeze_d').value = d;
|
|
||||||
document.getElementById('freeze_h').value = String(h).padStart(2, '0');
|
|
||||||
document.getElementById('freeze_m').value = String(m).padStart(2, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
function freezeTpToMinutes() {
|
|
||||||
const d = parseInt(document.getElementById('freeze_d').value) || 0;
|
|
||||||
const h = parseInt(document.getElementById('freeze_h').value) || 0;
|
|
||||||
const m = parseInt(document.getElementById('freeze_m').value) || 0;
|
|
||||||
return d * 24 * 60 + h * 60 + m;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openFreezeModal(lockId) {
|
function openFreezeModal(lockId) {
|
||||||
freezeTargetLockId = lockId;
|
freezeTargetLockId = lockId;
|
||||||
// Default: 4h
|
// Default: 4h
|
||||||
@@ -1405,7 +1257,7 @@
|
|||||||
|
|
||||||
async function submitFreeze() {
|
async function submitFreeze() {
|
||||||
const lockId = freezeTargetLockId;
|
const lockId = freezeTargetLockId;
|
||||||
const minutes = freezeTpToMinutes();
|
const minutes = tpToMinutes('freeze');
|
||||||
const errEl = document.getElementById('freezeModalError');
|
const errEl = document.getElementById('freezeModalError');
|
||||||
if (minutes < 1) {
|
if (minutes < 1) {
|
||||||
errEl.textContent = 'Bitte eine Dauer von mindestens 1 Minute angeben.';
|
errEl.textContent = 'Bitte eine Dauer von mindestens 1 Minute angeben.';
|
||||||
@@ -1485,26 +1337,6 @@
|
|||||||
let _pendingTaskIndex = null;
|
let _pendingTaskIndex = null;
|
||||||
let _pendingLockId = null;
|
let _pendingLockId = null;
|
||||||
|
|
||||||
function ctpFreezeTpChange(delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById('ctpf_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById('ctpf_h').value) || 0;
|
|
||||||
let m = parseInt(document.getElementById('ctpf_m').value) || 0;
|
|
||||||
if (seg === 'm') m += delta; else if (seg === 'h') h += delta; else d += delta;
|
|
||||||
if (m >= 60) { h += Math.floor(m/60); m = m%60; }
|
|
||||||
if (m < 0) { const b = Math.ceil(-m/60); h -= b; m += b*60; }
|
|
||||||
if (h >= 24) { d += Math.floor(h/24); h = h%24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById('ctpf_d').value = d;
|
|
||||||
document.getElementById('ctpf_h').value = String(h).padStart(2,'0');
|
|
||||||
document.getElementById('ctpf_m').value = String(m).padStart(2,'0');
|
|
||||||
}
|
|
||||||
|
|
||||||
function ctpFreezeToMinutes() {
|
|
||||||
return (parseInt(document.getElementById('ctpf_d').value)||0) * 24*60
|
|
||||||
+ (parseInt(document.getElementById('ctpf_h').value)||0) * 60
|
|
||||||
+ (parseInt(document.getElementById('ctpf_m').value)||0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ctpRedChange(delta) {
|
function ctpRedChange(delta) {
|
||||||
const inp = document.getElementById('ctpRedCount');
|
const inp = document.getElementById('ctpRedCount');
|
||||||
@@ -1566,7 +1398,7 @@
|
|||||||
}
|
}
|
||||||
document.getElementById('ctpPenaltyError').style.display = 'none';
|
document.getElementById('ctpPenaltyError').style.display = 'none';
|
||||||
|
|
||||||
const freezeVal = freezeEnabled ? (ctpFreezeToMinutes() || null) : null;
|
const freezeVal = freezeEnabled ? (tpToMinutes('ctpf') || null) : null;
|
||||||
const redVal = redEnabled ? (parseInt(document.getElementById('ctpRedCount').value) || null) : null;
|
const redVal = redEnabled ? (parseInt(document.getElementById('ctpRedCount').value) || null) : null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@@ -1865,32 +1697,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<div id="ctpPenaltyFreezeFields" style="padding-left:1.6rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
|
<div id="ctpPenaltyFreezeFields" style="padding-left:1.6rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('ctpf',-1,'d')">−</button><input type="text" id="ctpf_d" value="0" readonly><button type="button" onclick="tpChange('ctpf',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-seg-row">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('ctpf',-1,'h')">−</button><input type="text" id="ctpf_h" value="04" readonly><button type="button" onclick="tpChange('ctpf',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<button type="button" onclick="ctpFreezeTpChange(-1,'d')">−</button>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('ctpf',-1,'m')">−</button><input type="text" id="ctpf_m" value="00" readonly><button type="button" onclick="tpChange('ctpf',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<input type="text" id="ctpf_d" value="0" readonly>
|
|
||||||
<button type="button" onclick="ctpFreezeTpChange(1,'d')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Tage</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="ctpFreezeTpChange(-1,'h')">−</button>
|
|
||||||
<input type="text" id="ctpf_h" value="04" readonly>
|
|
||||||
<button type="button" onclick="ctpFreezeTpChange(1,'h')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Stunden</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="ctpFreezeTpChange(-1,'m')">−</button>
|
|
||||||
<input type="text" id="ctpf_m" value="00" readonly>
|
|
||||||
<button type="button" onclick="ctpFreezeTpChange(1,'m')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Minuten</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
|
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>Meine Vorlagen – xXx Sphere</title>
|
<title>Meine Vorlagen – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/time-picker.css">
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
/* ── Liste ── */
|
/* ── Liste ── */
|
||||||
@@ -133,26 +134,6 @@
|
|||||||
}
|
}
|
||||||
.stepper input[type="text"]:focus { outline:none; background:rgba(255,255,255,0.08); }
|
.stepper input[type="text"]:focus { outline:none; background:rgba(255,255,255,0.08); }
|
||||||
|
|
||||||
/* ── Zeitpicker ── */
|
|
||||||
.time-picker { display:flex; align-items:center; gap:0.4rem; flex-wrap:wrap; }
|
|
||||||
.tp-seg { display:flex; flex-direction:column; align-items:center; gap:0.15rem; }
|
|
||||||
.tp-seg-row { display:flex; align-items:center; gap:0.2rem; }
|
|
||||||
.tp-seg button {
|
|
||||||
width:24px; height:24px; background:var(--color-card);
|
|
||||||
border:1px solid var(--color-muted); border-radius:4px;
|
|
||||||
cursor:pointer; font-size:0.9rem; font-weight:700; color:var(--color-text);
|
|
||||||
display:flex; align-items:center; justify-content:center; padding:0; flex-shrink:0;
|
|
||||||
}
|
|
||||||
.tp-seg button:hover { background:var(--color-primary); color:#fff; border-color:var(--color-primary); }
|
|
||||||
.tp-seg input {
|
|
||||||
width:28px; text-align:center; background:var(--color-card);
|
|
||||||
border:1px solid var(--color-muted); border-radius:4px;
|
|
||||||
color:var(--color-text); font-size:0.9rem; font-weight:600;
|
|
||||||
font-family:monospace; padding:0.15rem 0; box-sizing:border-box;
|
|
||||||
}
|
|
||||||
.tp-seg .tp-label { font-size:0.62rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; }
|
|
||||||
.tp-colon { font-size:1rem; font-weight:700; color:var(--color-muted); margin-bottom:0.9rem; }
|
|
||||||
|
|
||||||
/* ── Aufgaben ── */
|
/* ── Aufgaben ── */
|
||||||
.task-list { display:flex; flex-direction:column; gap:0.5rem; margin-bottom:0.6rem; }
|
.task-list { display:flex; flex-direction:column; gap:0.5rem; margin-bottom:0.6rem; }
|
||||||
.task-item {
|
.task-item {
|
||||||
@@ -260,6 +241,33 @@
|
|||||||
.radio-group label { display:flex; align-items:center; gap:0.5rem; cursor:pointer; font-size:0.9rem; margin:0; color:var(--color-text); }
|
.radio-group label { display:flex; align-items:center; gap:0.5rem; cursor:pointer; font-size:0.9rem; margin:0; color:var(--color-text); }
|
||||||
.radio-group input[type="radio"] { width:auto; padding:0; margin:0; }
|
.radio-group input[type="radio"] { width:auto; padding:0; margin:0; }
|
||||||
|
|
||||||
|
/* ── Spiel-Set Suche ── */
|
||||||
|
.gs-dropdown {
|
||||||
|
display:none; position:absolute; top:100%; left:0; right:0; z-index:200;
|
||||||
|
background:var(--color-card); border:1px solid var(--color-secondary);
|
||||||
|
border-radius:6px; max-height:200px; overflow-y:auto;
|
||||||
|
box-shadow:0 4px 14px rgba(0,0,0,0.35); margin-top:2px;
|
||||||
|
}
|
||||||
|
.gs-dropdown-item {
|
||||||
|
padding:0.55rem 0.9rem; cursor:pointer; font-size:0.9rem;
|
||||||
|
border-bottom:1px solid rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
.gs-dropdown-item:last-child { border-bottom:none; }
|
||||||
|
.gs-dropdown-item:hover { background:rgba(255,255,255,0.07); }
|
||||||
|
.gs-item-name { font-weight:600; color:var(--color-text); }
|
||||||
|
.gs-item-desc { font-size:0.78rem; color:var(--color-muted); margin-top:0.1rem; }
|
||||||
|
.gs-selected {
|
||||||
|
display:flex; align-items:center; gap:0.6rem;
|
||||||
|
background:rgba(255,255,255,0.05); border:1px solid var(--color-secondary);
|
||||||
|
border-radius:6px; padding:0.45rem 0.75rem; margin-top:0.35rem;
|
||||||
|
font-size:0.88rem; color:var(--color-text);
|
||||||
|
}
|
||||||
|
.gs-selected button {
|
||||||
|
background:none; border:none; color:var(--color-muted);
|
||||||
|
cursor:pointer; padding:0; margin:0; font-size:1rem; width:auto; line-height:1;
|
||||||
|
}
|
||||||
|
.gs-selected button:hover { color:#e74c3c; background:none; }
|
||||||
|
|
||||||
/* ── Simulation ── */
|
/* ── Simulation ── */
|
||||||
.sim-bar-track { background:var(--color-secondary); border-radius:6px; height:8px; overflow:hidden; margin:0.5rem 0 0.25rem; }
|
.sim-bar-track { background:var(--color-secondary); border-radius:6px; height:8px; overflow:hidden; margin:0.5rem 0 0.25rem; }
|
||||||
.sim-bar-fill { height:100%; background:var(--color-primary); border-radius:6px; transition:width 0.1s linear; width:0%; }
|
.sim-bar-fill { height:100%; background:var(--color-primary); border-radius:6px; transition:width 0.1s linear; width:0%; }
|
||||||
@@ -353,32 +361,9 @@
|
|||||||
<div class="form-row" style="margin-top:0.75rem;">
|
<div class="form-row" style="margin-top:0.75rem;">
|
||||||
<label>Karte ziehen alle</label>
|
<label>Karte ziehen alle</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pe',-1,'d')">−</button><input type="text" id="pe_d" value="0" readonly><button type="button" onclick="tpChange('pe',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-seg-row">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pe',-1,'h')">−</button><input type="text" id="pe_h" value="01" readonly><button type="button" onclick="tpChange('pe',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<button type="button" onclick="tpChange('pe',-1,'d')">−</button>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pe',-1,'m')">−</button><input type="text" id="pe_m" value="00" readonly><button type="button" onclick="tpChange('pe',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<input type="text" id="pe_d" value="0" readonly>
|
|
||||||
<button type="button" onclick="tpChange('pe',1,'d')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Tage</span>
|
|
||||||
</div>
|
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="tpChange('pe',-1,'h')">−</button>
|
|
||||||
<input type="text" id="pe_h" value="01" readonly>
|
|
||||||
<button type="button" onclick="tpChange('pe',1,'h')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Std</span>
|
|
||||||
</div>
|
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="tpChange('pe',-1,'m')">−</button>
|
|
||||||
<input type="text" id="pe_m" value="00" readonly>
|
|
||||||
<button type="button" onclick="tpChange('pe',1,'m')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Min</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -391,25 +376,55 @@
|
|||||||
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
|
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Aufgaben (CardLock) -->
|
<!-- Aufgaben-Set – sichtbar wenn TASK > 0 oder GAME_CARD > 0 -->
|
||||||
<div class="form-section">
|
<div id="sectionAufgabenSet" style="display:none;">
|
||||||
<div class="form-section-title">Aufgaben (optional)</div>
|
<div class="form-section">
|
||||||
<div style="margin-bottom:0.65rem;">
|
<div class="form-section-title">Aufgaben-Set</div>
|
||||||
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgabe?</div>
|
|
||||||
<div class="radio-group">
|
<!-- Task-Karte: Entscheider + internes Set -->
|
||||||
<label><input type="radio" name="modalCardTaskMode" value="RANDOM" checked> Zufall</label>
|
<div id="subCardTaskSet" style="display:none;">
|
||||||
<label><input type="radio" name="modalCardTaskMode" value="KEYHOLDER" > Keyholder*In</label>
|
<div style="margin-bottom:0.65rem;">
|
||||||
<label><input type="radio" name="modalCardTaskMode" value="COMMUNITY" > Community</label>
|
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgabe?</div>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="modalCardTaskMode" value="RANDOM" checked> Zufall</label>
|
||||||
|
<label><input type="radio" name="modalCardTaskMode" value="KEYHOLDER" > Keyholder*In</label>
|
||||||
|
<label><input type="radio" name="modalCardTaskMode" value="COMMUNITY" > Community</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row" style="margin-bottom:0.5rem;">
|
||||||
|
<label>Aufgaben-Set <span class="required-star">*</span></label>
|
||||||
|
<select id="fCardTaskSetId" onchange="onTaskSetChange('card')">
|
||||||
|
<option value="">Kein Aufgaben-Set</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn-add" type="button" onclick="openTaskSetModal(null,'card')">+ Neues Set anlegen</button>
|
||||||
|
<div id="cardTaskSetPreview" class="task-set-preview"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Spiel-Karte: Chastity-Set + Spieldauer -->
|
||||||
|
<div id="subGameSet" style="display:none;">
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
|
||||||
|
<div style="position:relative;">
|
||||||
|
<input type="text" id="gameSetSearch" placeholder="Name eingeben zum Suchen…"
|
||||||
|
autocomplete="off" oninput="onGameSetSearch(this.value)" onfocus="onGameSetSearchFocus()">
|
||||||
|
<div id="gameSetDropdown" class="gs-dropdown"></div>
|
||||||
|
</div>
|
||||||
|
<div id="gameSetSelected" class="gs-selected" style="display:none;"></div>
|
||||||
|
<input type="hidden" id="fGameSetId">
|
||||||
|
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.4rem;">
|
||||||
|
<label for="sldGameSpieldauer" style="margin:0;">Spieldauer</label>
|
||||||
|
<span id="valGameSpieldauer" style="font-size:0.85rem;color:var(--color-muted);">Mittel</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2"
|
||||||
|
oninput="updateGameSpieldauer(this.value)"
|
||||||
|
style="width:100%;accent-color:var(--color-primary);">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="margin-bottom:0.5rem;">
|
|
||||||
<label>Aufgaben-Set</label>
|
|
||||||
<select id="fCardTaskSetId" onchange="onTaskSetChange('card')">
|
|
||||||
<option value="">Kein Aufgaben-Set</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button class="btn-add" type="button" onclick="openTaskSetModal(null,'card')">+ Neues Set anlegen</button>
|
|
||||||
<div id="cardTaskSetPreview" class="task-set-preview"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -423,20 +438,16 @@
|
|||||||
<label>Mindestdauer (optional)</label>
|
<label>Mindestdauer (optional)</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'d')">−</button><input type="text" id="tmin_d" value="0" readonly><button type="button" onclick="tpChange('tmin',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('tmin',-1,'d')">−</button><input type="text" id="tmin_d" value="0" readonly><button type="button" onclick="tpChange('tmin',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'h')">−</button><input type="text" id="tmin_h" value="00" readonly><button type="button" onclick="tpChange('tmin',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'h')">−</button><input type="text" id="tmin_h" value="00" readonly><button type="button" onclick="tpChange('tmin',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'m')">−</button><input type="text" id="tmin_m" value="00" readonly><button type="button" onclick="tpChange('tmin',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'m')">−</button><input type="text" id="tmin_m" value="00" readonly><button type="button" onclick="tpChange('tmin',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" id="rowMaxTime">
|
<div class="form-row" id="rowMaxTime">
|
||||||
<label>Maximaldauer<span class="required-star">*</span></label>
|
<label>Maximaldauer<span class="required-star">*</span></label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'d')">−</button><input type="text" id="tmax_d" value="0" readonly><button type="button" onclick="tpChange('tmax',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('tmax',-1,'d')">−</button><input type="text" id="tmax_d" value="0" readonly><button type="button" onclick="tpChange('tmax',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'h')">−</button><input type="text" id="tmax_h" value="01" readonly><button type="button" onclick="tpChange('tmax',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'h')">−</button><input type="text" id="tmax_h" value="01" readonly><button type="button" onclick="tpChange('tmax',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'m')">−</button><input type="text" id="tmax_m" value="00" readonly><button type="button" onclick="tpChange('tmax',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'m')">−</button><input type="text" id="tmax_m" value="00" readonly><button type="button" onclick="tpChange('tmax',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox-row">
|
<div class="checkbox-row">
|
||||||
@@ -457,10 +468,8 @@
|
|||||||
<label>Rad drehen alle</label>
|
<label>Rad drehen alle</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'d')">−</button><input type="text" id="se_d" value="0" readonly><button type="button" onclick="tpChange('se',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('se',-1,'d')">−</button><input type="text" id="se_d" value="0" readonly><button type="button" onclick="tpChange('se',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'h')">−</button><input type="text" id="se_h" value="01" readonly><button type="button" onclick="tpChange('se',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'h')">−</button><input type="text" id="se_h" value="01" readonly><button type="button" onclick="tpChange('se',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'m')">−</button><input type="text" id="se_m" value="00" readonly><button type="button" onclick="tpChange('se',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'m')">−</button><input type="text" id="se_m" value="00" readonly><button type="button" onclick="tpChange('se',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
@@ -486,10 +495,8 @@
|
|||||||
<label>Aufgaben alle</label>
|
<label>Aufgaben alle</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'d')">−</button><input type="text" id="te_d" value="0" readonly><button type="button" onclick="tpChange('te',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('te',-1,'d')">−</button><input type="text" id="te_d" value="0" readonly><button type="button" onclick="tpChange('te',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'h')">−</button><input type="text" id="te_h" value="08" readonly><button type="button" onclick="tpChange('te',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'h')">−</button><input type="text" id="te_h" value="08" readonly><button type="button" onclick="tpChange('te',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'m')">−</button><input type="text" id="te_m" value="00" readonly><button type="button" onclick="tpChange('te',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'m')">−</button><input type="text" id="te_m" value="00" readonly><button type="button" onclick="tpChange('te',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="margin-bottom:0;">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
@@ -533,10 +540,8 @@
|
|||||||
<label>Dauer</label>
|
<label>Dauer</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'d')">−</button><input type="text" id="pv_d" value="0" readonly><button type="button" onclick="tpChange('pv',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('pv',-1,'d')">−</button><input type="text" id="pv_d" value="0" readonly><button type="button" onclick="tpChange('pv',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'h')">−</button><input type="text" id="pv_h" value="01" readonly><button type="button" onclick="tpChange('pv',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'h')">−</button><input type="text" id="pv_h" value="01" readonly><button type="button" onclick="tpChange('pv',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'m')">−</button><input type="text" id="pv_m" value="00" readonly><button type="button" onclick="tpChange('pv',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'m')">−</button><input type="text" id="pv_m" value="00" readonly><button type="button" onclick="tpChange('pv',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,20 +559,16 @@
|
|||||||
<label>Hygiene-Öffnung alle</label>
|
<label>Hygiene-Öffnung alle</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'d')">−</button><input type="text" id="he_d" value="1" readonly><button type="button" onclick="tpChange('he',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('he',-1,'d')">−</button><input type="text" id="he_d" value="1" readonly><button type="button" onclick="tpChange('he',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'h')">−</button><input type="text" id="he_h" value="00" readonly><button type="button" onclick="tpChange('he',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'h')">−</button><input type="text" id="he_h" value="00" readonly><button type="button" onclick="tpChange('he',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'m')">−</button><input type="text" id="he_m" value="00" readonly><button type="button" onclick="tpChange('he',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'m')">−</button><input type="text" id="he_m" value="00" readonly><button type="button" onclick="tpChange('he',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="margin-bottom:0;">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<label>Dauer der Öffnung</label>
|
<label>Dauer der Öffnung</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'d')">−</button><input type="text" id="hd_d" value="0" readonly><button type="button" onclick="tpChange('hd',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('hd',-1,'d')">−</button><input type="text" id="hd_d" value="0" readonly><button type="button" onclick="tpChange('hd',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'h')">−</button><input type="text" id="hd_h" value="00" readonly><button type="button" onclick="tpChange('hd',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'h')">−</button><input type="text" id="hd_h" value="00" readonly><button type="button" onclick="tpChange('hd',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'m')">−</button><input type="text" id="hd_m" value="30" readonly><button type="button" onclick="tpChange('hd',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'m')">−</button><input type="text" id="hd_m" value="30" readonly><button type="button" onclick="tpChange('hd',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -652,8 +653,9 @@
|
|||||||
<script src="/js/card-defs.js"></script>
|
<script src="/js/card-defs.js"></script>
|
||||||
<script src="/js/card-display.js"></script>
|
<script src="/js/card-display.js"></script>
|
||||||
<script src="/js/icons.js"></script>
|
<script src="/js/icons.js"></script>
|
||||||
<script src="/js/nav.js"></script>
|
<script src="/js/nav.js"></script>
|
||||||
<script src="/js/social-sidebar.js"></script>
|
<script src="/js/social-sidebar.js"></script>
|
||||||
|
<script src="/js/time-picker.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||||
|
|
||||||
@@ -673,36 +675,6 @@
|
|||||||
let isLastPage = false;
|
let isLastPage = false;
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
|
|
||||||
// ── Zeitpicker ──
|
|
||||||
function tpChange(prefix, delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById(prefix + '_h').value) || 0;
|
|
||||||
let m = parseInt(document.getElementById(prefix + '_m').value) || 0;
|
|
||||||
if (seg === 'm') m += delta;
|
|
||||||
else if (seg === 'h') h += delta;
|
|
||||||
else d += delta;
|
|
||||||
if (m >= 60) { h += Math.floor(m/60); m %= 60; }
|
|
||||||
if (m < 0) { const b = Math.ceil(-m/60); h -= b; m += b*60; }
|
|
||||||
if (h >= 24) { d += Math.floor(h/24); h %= 24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById(prefix + '_d').value = d;
|
|
||||||
document.getElementById(prefix + '_h').value = String(h).padStart(2,'0');
|
|
||||||
document.getElementById(prefix + '_m').value = String(m).padStart(2,'0');
|
|
||||||
}
|
|
||||||
function tpToMinutes(prefix) {
|
|
||||||
const d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
|
||||||
const h = parseInt(document.getElementById(prefix + '_h').value) || 0;
|
|
||||||
const m = parseInt(document.getElementById(prefix + '_m').value) || 0;
|
|
||||||
return d*1440 + h*60 + m;
|
|
||||||
}
|
|
||||||
function tpFromMinutes(prefix, total) {
|
|
||||||
total = total || 0;
|
|
||||||
const d = Math.floor(total/1440), h = Math.floor((total%1440)/60), m = total%60;
|
|
||||||
document.getElementById(prefix + '_d').value = d;
|
|
||||||
document.getElementById(prefix + '_h').value = String(h).padStart(2,'0');
|
|
||||||
document.getElementById(prefix + '_m').value = String(m).padStart(2,'0');
|
|
||||||
}
|
|
||||||
function fmtMinutes(min) {
|
function fmtMinutes(min) {
|
||||||
if (!min) return '–';
|
if (!min) return '–';
|
||||||
const d = Math.floor(min/1440), h = Math.floor((min%1440)/60), m = min%60;
|
const d = Math.floor(min/1440), h = Math.floor((min%1440)/60), m = min%60;
|
||||||
@@ -757,8 +729,134 @@
|
|||||||
function syncMinMax(id, val) {
|
function syncMinMax(id, val) {
|
||||||
if (id.startsWith('min_')) { const mx = document.getElementById('max_'+id.slice(4)); if (mx && val > (parseInt(mx.value)||0)) mx.value = val; }
|
if (id.startsWith('min_')) { const mx = document.getElementById('max_'+id.slice(4)); if (mx && val > (parseInt(mx.value)||0)) mx.value = val; }
|
||||||
else if (id.startsWith('max_')) { const mn = document.getElementById('min_'+id.slice(4)); if (mn && val < (parseInt(mn.value)||0)) mn.value = val; }
|
else if (id.startsWith('max_')) { const mn = document.getElementById('min_'+id.slice(4)); if (mn && val < (parseInt(mn.value)||0)) mn.value = val; }
|
||||||
|
if (id === 'min_GAME_CARD' || id === 'max_GAME_CARD') {
|
||||||
|
if (val > 0) {
|
||||||
|
// Gegenseitiger Ausschluss: Task-Karten nullen
|
||||||
|
const minT = document.getElementById('min_TASK'); if (minT) minT.value = 0;
|
||||||
|
const maxT = document.getElementById('max_TASK'); if (maxT) maxT.value = 0;
|
||||||
|
checkTaskCardSection();
|
||||||
|
}
|
||||||
|
checkGameCardSection();
|
||||||
|
}
|
||||||
|
if (id === 'min_TASK' || id === 'max_TASK') {
|
||||||
|
if (val > 0) {
|
||||||
|
// Gegenseitiger Ausschluss: Game-Karten nullen
|
||||||
|
const minG = document.getElementById('min_GAME_CARD'); if (minG) minG.value = 0;
|
||||||
|
const maxG = document.getElementById('max_GAME_CARD'); if (maxG) maxG.value = 0;
|
||||||
|
checkGameCardSection();
|
||||||
|
}
|
||||||
|
checkTaskCardSection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Spiel-Karte: Aufgaben-Set + Spieldauer ──
|
||||||
|
const GAME_SPIELDAUER = [
|
||||||
|
{ label: 'Sehr kurz' },
|
||||||
|
{ label: 'Kurz' },
|
||||||
|
{ label: 'Mittel' },
|
||||||
|
{ label: 'Lang' },
|
||||||
|
{ label: 'Sehr lang' },
|
||||||
|
];
|
||||||
|
let _gameSetSearchTimer = null;
|
||||||
|
let _gameSetResults = [];
|
||||||
|
|
||||||
|
function checkGameCardSection() {
|
||||||
|
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
|
||||||
|
const maxV = parseInt(document.getElementById('max_GAME_CARD')?.value) || 0;
|
||||||
|
const hasGame = minV > 0 || maxV > 0;
|
||||||
|
const sub = document.getElementById('subGameSet');
|
||||||
|
if (sub) sub.style.display = hasGame ? '' : 'none';
|
||||||
|
checkAufgabenSetSection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkTaskCardSection() {
|
||||||
|
const minV = parseInt(document.getElementById('min_TASK')?.value) || 0;
|
||||||
|
const maxV = parseInt(document.getElementById('max_TASK')?.value) || 0;
|
||||||
|
const hasTask = minV > 0 || maxV > 0;
|
||||||
|
const sub = document.getElementById('subCardTaskSet');
|
||||||
|
if (sub) sub.style.display = hasTask ? '' : 'none';
|
||||||
|
checkAufgabenSetSection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAufgabenSetSection() {
|
||||||
|
const hasTask = (parseInt(document.getElementById('min_TASK')?.value) || 0) > 0
|
||||||
|
|| (parseInt(document.getElementById('max_TASK')?.value) || 0) > 0;
|
||||||
|
const hasGame = (parseInt(document.getElementById('min_GAME_CARD')?.value) || 0) > 0
|
||||||
|
|| (parseInt(document.getElementById('max_GAME_CARD')?.value) || 0) > 0;
|
||||||
|
const sec = document.getElementById('sectionAufgabenSet');
|
||||||
|
if (sec) sec.style.display = (hasTask || hasGame) ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGameSpieldauer(val) {
|
||||||
|
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[+val].label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGameSetSearchFocus() {
|
||||||
|
if (!document.getElementById('fGameSetId').value) onGameSetSearch(document.getElementById('gameSetSearch').value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGameSetSearch(value) {
|
||||||
|
clearTimeout(_gameSetSearchTimer);
|
||||||
|
if (value.length === 0) {
|
||||||
|
_gameSetSearchTimer = setTimeout(() => doGameSetSearch(''), 0);
|
||||||
|
} else if (value.length < 2) {
|
||||||
|
document.getElementById('gameSetDropdown').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
_gameSetSearchTimer = setTimeout(() => doGameSetSearch(value), 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doGameSetSearch(search) {
|
||||||
|
try {
|
||||||
|
const url = '/gruppe/chastity' + (search ? '?search=' + encodeURIComponent(search) : '');
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
_gameSetResults = data.gruppen || [];
|
||||||
|
renderGameSetDropdown();
|
||||||
|
} catch(e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGameSetDropdown() {
|
||||||
|
const dd = document.getElementById('gameSetDropdown');
|
||||||
|
if (!dd) return;
|
||||||
|
if (!_gameSetResults.length) { dd.style.display = 'none'; return; }
|
||||||
|
dd.innerHTML = _gameSetResults.map(g => `
|
||||||
|
<div class="gs-dropdown-item" onclick="selectGameSet('${esc(g.gruppenId)}','${esc(g.name).replace(/'/g, "\\'")}')">
|
||||||
|
<div class="gs-item-name">${esc(g.name)}</div>
|
||||||
|
${g.beschreibung ? `<div class="gs-item-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||||
|
</div>`).join('');
|
||||||
|
dd.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectGameSet(id, name, suppressDirty = false) {
|
||||||
|
document.getElementById('fGameSetId').value = id;
|
||||||
|
document.getElementById('gameSetSearch').value = '';
|
||||||
|
document.getElementById('gameSetDropdown').style.display = 'none';
|
||||||
|
document.getElementById('gameSetSelected').innerHTML =
|
||||||
|
`<span style="flex:1;">${esc(name)}</span>
|
||||||
|
<button type="button" onclick="clearGameSet()" title="Auswahl entfernen">✕</button>`;
|
||||||
|
document.getElementById('gameSetSelected').style.display = 'flex';
|
||||||
|
document.getElementById('errGameSet').style.display = 'none';
|
||||||
|
if (!suppressDirty) markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearGameSet() {
|
||||||
|
document.getElementById('fGameSetId').value = '';
|
||||||
|
document.getElementById('gameSetSearch').value = '';
|
||||||
|
document.getElementById('gameSetSelected').style.display = 'none';
|
||||||
|
document.getElementById('gameSetSelected').innerHTML = '';
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const search = document.getElementById('gameSetSearch');
|
||||||
|
const dd = document.getElementById('gameSetDropdown');
|
||||||
|
if (dd && search && !search.contains(e.target) && !dd.contains(e.target)) {
|
||||||
|
dd.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── Karten-Info ──
|
// ── Karten-Info ──
|
||||||
function openCardInfo(cardId) {
|
function openCardInfo(cardId) {
|
||||||
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
|
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
|
||||||
@@ -815,10 +913,8 @@
|
|||||||
<div id="we-tp-${id}" style="display:none;">
|
<div id="we-tp-${id}" style="display:none;">
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'d')">−</button><input type="text" id="wt${id}_d" value="0" readonly><button type="button" onclick="tpChange('wt${id}',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('wt${id}',-1,'d')">−</button><input type="text" id="wt${id}_d" value="0" readonly><button type="button" onclick="tpChange('wt${id}',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'h')">−</button><input type="text" id="wt${id}_h" value="01" readonly><button type="button" onclick="tpChange('wt${id}',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'h')">−</button><input type="text" id="wt${id}_h" value="01" readonly><button type="button" onclick="tpChange('wt${id}',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'m')">−</button><input type="text" id="wt${id}_m" value="00" readonly><button type="button" onclick="tpChange('wt${id}',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'m')">−</button><input type="text" id="wt${id}_m" value="00" readonly><button type="button" onclick="tpChange('wt${id}',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="we-str-${id}" placeholder="Text…" maxlength="200"
|
<input type="text" id="we-str-${id}" placeholder="Text…" maxlength="200"
|
||||||
@@ -1163,7 +1259,7 @@
|
|||||||
function alignModalToContent() {
|
function alignModalToContent() {
|
||||||
const rect = document.querySelector('.content')?.getBoundingClientRect();
|
const rect = document.querySelector('.content')?.getBoundingClientRect();
|
||||||
if (!rect) return;
|
if (!rect) return;
|
||||||
document.getElementById('modalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 720) + 'px';
|
document.getElementById('modalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 800) + 'px';
|
||||||
document.getElementById('taskSetModalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 900) + 'px';
|
document.getElementById('taskSetModalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 900) + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1201,6 +1297,20 @@
|
|||||||
tpFromMinutes('pe', template?.pickEveryMinute || 60);
|
tpFromMinutes('pe', template?.pickEveryMinute || 60);
|
||||||
document.getElementById('fAccumulate').checked = template?.accumulatePicks || false;
|
document.getElementById('fAccumulate').checked = template?.accumulatePicks || false;
|
||||||
document.getElementById('fShowRemaining').checked = template?.showRemainingCards || false;
|
document.getElementById('fShowRemaining').checked = template?.showRemainingCards || false;
|
||||||
|
|
||||||
|
// Task-Karte und Spiel-Karte
|
||||||
|
checkTaskCardSection();
|
||||||
|
clearGameSet();
|
||||||
|
checkGameCardSection();
|
||||||
|
const gsi = template?.gameSpieldauerIdx ?? 2;
|
||||||
|
document.getElementById('sldGameSpieldauer').value = gsi;
|
||||||
|
updateGameSpieldauer(gsi);
|
||||||
|
if (template?.gameSetId) {
|
||||||
|
fetch(`/gruppe/${template.gameSetId}`)
|
||||||
|
.then(r => r.ok ? r.json() : null)
|
||||||
|
.then(g => { if (g?.name) selectGameSet(template.gameSetId, g.name, true); })
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'TIMELOCK') {
|
if (type === 'TIMELOCK') {
|
||||||
@@ -1350,7 +1460,15 @@
|
|||||||
const totalMax = CARD_DEFS.reduce((s,c)=>s+(parseInt(document.getElementById('max_'+c.id).value)||0),0);
|
const totalMax = CARD_DEFS.reduce((s,c)=>s+(parseInt(document.getElementById('max_'+c.id).value)||0),0);
|
||||||
if (totalMax===0) { showModalError('Das Deck muss mindestens eine Karte enthalten.'); firstError=firstError||document.getElementById('modalError'); }
|
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;
|
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'); }
|
if (hasTaskCards && !document.getElementById('fCardTaskSetId').value) { showModalError('Bitte ein Aufgaben-Set für die Aufgaben-Karten auswählen.'); 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) {
|
||||||
|
document.getElementById('errGameSet').style.display = '';
|
||||||
|
showModalError('Spiel-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.');
|
||||||
|
firstError = firstError || document.getElementById('errGameSet');
|
||||||
|
} else {
|
||||||
|
document.getElementById('errGameSet').style.display = 'none';
|
||||||
|
}
|
||||||
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
|
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
|
||||||
|
|
||||||
const cardCountsMin={}, cardCountsMax={};
|
const cardCountsMin={}, cardCountsMax={};
|
||||||
@@ -1369,6 +1487,8 @@
|
|||||||
taskSetId: document.getElementById('fCardTaskSetId').value || null,
|
taskSetId: document.getElementById('fCardTaskSetId').value || null,
|
||||||
requiresVerification: document.getElementById('fRequiresVerification').checked,
|
requiresVerification: document.getElementById('fRequiresVerification').checked,
|
||||||
taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM',
|
taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM',
|
||||||
|
gameSetId: hasGameCards ? (document.getElementById('fGameSetId').value || null) : null,
|
||||||
|
gameSpieldauerIdx: hasGameCards ? (parseInt(document.getElementById('sldGameSpieldauer').value) || 2) : null,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// TimeLock
|
// TimeLock
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>Neues Lock – xXx Sphere</title>
|
<title>Neues Lock – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/time-picker.css">
|
||||||
<style>
|
<style>
|
||||||
.form-section {
|
.form-section {
|
||||||
background: var(--color-card);
|
background: var(--color-card);
|
||||||
@@ -108,26 +109,6 @@
|
|||||||
.field-error input { border-color: #e74c3c !important; }
|
.field-error input { border-color: #e74c3c !important; }
|
||||||
.field-error-msg { font-size: 0.78rem; color: #e74c3c; margin-top: 0.15rem; }
|
.field-error-msg { font-size: 0.78rem; color: #e74c3c; margin-top: 0.15rem; }
|
||||||
|
|
||||||
/* Zeitpicker */
|
|
||||||
.time-picker { display:flex; align-items:center; gap:0.4rem; flex-wrap:wrap; }
|
|
||||||
.tp-seg { display:flex; flex-direction:column; align-items:center; gap:0.15rem; }
|
|
||||||
.tp-seg-row { display:flex; align-items:center; gap:0.2rem; }
|
|
||||||
.tp-seg button {
|
|
||||||
width:24px; height:24px; background:var(--color-card);
|
|
||||||
border:1px solid var(--color-muted); border-radius:4px;
|
|
||||||
cursor:pointer; font-size:0.9rem; font-weight:700; color:var(--color-text);
|
|
||||||
display:flex; align-items:center; justify-content:center; padding:0; flex-shrink:0;
|
|
||||||
}
|
|
||||||
.tp-seg button:hover { background:var(--color-primary); color:#fff; border-color:var(--color-primary); }
|
|
||||||
.tp-seg input {
|
|
||||||
width:28px; text-align:center; background:var(--color-card);
|
|
||||||
border:1px solid var(--color-muted); border-radius:4px;
|
|
||||||
color:var(--color-text); font-size:0.9rem; font-weight:600;
|
|
||||||
font-family:monospace; padding:0.15rem 0; box-sizing:border-box;
|
|
||||||
}
|
|
||||||
.tp-seg .tp-label { font-size:0.62rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; }
|
|
||||||
.tp-colon { font-size:1rem; font-weight:700; color:var(--color-muted); margin-bottom:0.9rem; }
|
|
||||||
|
|
||||||
/* LockControl-Auswahl */
|
/* LockControl-Auswahl */
|
||||||
.lockcontrol-options { display: flex; flex-direction: column; gap: 0.6rem; }
|
.lockcontrol-options { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||||
.lockcontrol-option {
|
.lockcontrol-option {
|
||||||
@@ -225,23 +206,8 @@
|
|||||||
<div class="form-row" id="rowMaxDuration">
|
<div class="form-row" id="rowMaxDuration">
|
||||||
<label>Längste Dauer</label>
|
<label>Längste Dauer</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<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-row">
|
<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>
|
||||||
<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-colon">:</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">Std</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-hint">Das Lock öffnet spätestens nach dieser Zeit automatisch. 0 : 00 = keine Begrenzung.</div>
|
<div class="form-hint">Das Lock öffnet spätestens nach dieser Zeit automatisch. 0 : 00 = keine Begrenzung.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -359,8 +325,9 @@
|
|||||||
<script src="/js/card-defs.js"></script>
|
<script src="/js/card-defs.js"></script>
|
||||||
<script src="/js/shared.js"></script>
|
<script src="/js/shared.js"></script>
|
||||||
<script src="/js/icons.js"></script>
|
<script src="/js/icons.js"></script>
|
||||||
<script src="/js/nav.js"></script>
|
<script src="/js/nav.js"></script>
|
||||||
<script src="/js/social-sidebar.js"></script>
|
<script src="/js/social-sidebar.js"></script>
|
||||||
|
<script src="/js/time-picker.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let myUserId = null;
|
let myUserId = null;
|
||||||
let myUserName = null;
|
let myUserName = null;
|
||||||
@@ -655,20 +622,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Zeitpicker ──
|
|
||||||
function tpChange(prefix, delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById(prefix + '_h')?.value) || 0;
|
|
||||||
if (seg === 'h') h += delta;
|
|
||||||
else d += delta;
|
|
||||||
if (h >= 24) { d += Math.floor(h / 24); h %= 24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h / 24); d -= b; h += b * 24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById(prefix + '_d').value = d;
|
|
||||||
if (document.getElementById(prefix + '_h'))
|
|
||||||
document.getElementById(prefix + '_h').value = String(h).padStart(2, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Längste Dauer → LocalDateTime ──
|
// ── Längste Dauer → LocalDateTime ──
|
||||||
function durationToLatestOpening() {
|
function durationToLatestOpening() {
|
||||||
const days = parseInt(document.getElementById('dur_d').value) || 0;
|
const days = parseInt(document.getElementById('dur_d').value) || 0;
|
||||||
@@ -843,6 +796,8 @@
|
|||||||
requiresVerification: t.requiresVerification,
|
requiresVerification: t.requiresVerification,
|
||||||
testLock: isTestLock,
|
testLock: isTestLock,
|
||||||
controllType: selectedLockControl,
|
controllType: selectedLockControl,
|
||||||
|
gameSetId: t.gameSetId || null,
|
||||||
|
gameSpieldauerIdx: t.gameSpieldauerIdx ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@
|
|||||||
<h2>Level 6 erreicht!</h2>
|
<h2>Level 6 erreicht!</h2>
|
||||||
<div class="game-label" id="finisherTitle"></div>
|
<div class="game-label" id="finisherTitle"></div>
|
||||||
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
|
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
|
||||||
<button class="btn-secondary" onclick="goBack()" style="margin-top:1.25rem;">Zurück zum Lock</button>
|
<button class="btn-primary" onclick="completeGame()" style="margin-top:1.25rem;">✓ Spiel beenden</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -189,8 +189,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const lockId = params.get('lockId');
|
const lockId = params.get('lockId');
|
||||||
|
const autoGameSetId = params.get('gameSetId');
|
||||||
let _state = null;
|
let _state = null;
|
||||||
let _timerInt = null;
|
let _timerInt = null;
|
||||||
let _pendingIsLock = false;
|
let _pendingIsLock = false;
|
||||||
@@ -201,13 +202,23 @@
|
|||||||
else history.back();
|
else history.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function completeGame() {
|
||||||
|
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
|
||||||
|
await fetch(url, { method: 'POST' });
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Init ──────────────────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/lock-game/state');
|
const r = await fetch('/lock-game/state');
|
||||||
if (r.status === 404) {
|
if (r.status === 404) {
|
||||||
await loadGroups();
|
if (autoGameSetId) {
|
||||||
|
await autoStartGame(autoGameSetId);
|
||||||
|
} else {
|
||||||
|
await loadGroups();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
|
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
|
||||||
@@ -219,6 +230,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function autoStartGame(gameSetId) {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + gameSetId, { method: 'POST' });
|
||||||
|
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||||
|
const stateR = await fetch('/lock-game/state');
|
||||||
|
_state = await stateR.json();
|
||||||
|
hide('loadingHint');
|
||||||
|
await runGameLoop();
|
||||||
|
} catch (e) {
|
||||||
|
showError(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadGroups() {
|
async function loadGroups() {
|
||||||
const r = await fetch('/lock-game/groups');
|
const r = await fetch('/lock-game/groups');
|
||||||
const groups = r.ok ? await r.json() : [];
|
const groups = r.ok ? await r.json() : [];
|
||||||
|
|||||||
BIN
bin/main/static/img/icons/bell.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
bin/main/static/img/icons/envelope.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
bin/main/static/img/icons/lock.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
bin/main/static/img/icons/stars.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
54
bin/main/static/js/time-picker.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
function tpChange(prefix, delta, seg) {
|
||||||
|
let d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
||||||
|
let h = parseInt(document.getElementById(prefix + '_h')?.value) || 0;
|
||||||
|
let m = parseInt(document.getElementById(prefix + '_m')?.value) || 0;
|
||||||
|
if (seg === 'm') m += delta;
|
||||||
|
else if (seg === 'h') h += delta;
|
||||||
|
else d += delta;
|
||||||
|
if (m >= 60) { h += Math.floor(m/60); m %= 60; }
|
||||||
|
if (m < 0) { const b = Math.ceil(-m/60); h -= b; m += b*60; }
|
||||||
|
if (h >= 24) { d += Math.floor(h/24); h %= 24; }
|
||||||
|
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
|
||||||
|
if (d < 0) d = 0;
|
||||||
|
document.getElementById(prefix + '_d').value = d;
|
||||||
|
if (document.getElementById(prefix + '_h'))
|
||||||
|
document.getElementById(prefix + '_h').value = String(h).padStart(2, '0');
|
||||||
|
if (document.getElementById(prefix + '_m'))
|
||||||
|
document.getElementById(prefix + '_m').value = String(m).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tpToMinutes(prefix) {
|
||||||
|
const d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
||||||
|
const h = parseInt(document.getElementById(prefix + '_h')?.value) || 0;
|
||||||
|
const m = parseInt(document.getElementById(prefix + '_m')?.value) || 0;
|
||||||
|
return d * 1440 + h * 60 + m;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tpFromMinutes(prefix, total) {
|
||||||
|
total = total || 0;
|
||||||
|
const d = Math.floor(total / 1440);
|
||||||
|
const h = Math.floor((total % 1440) / 60);
|
||||||
|
const m = total % 60;
|
||||||
|
document.getElementById(prefix + '_d').value = d;
|
||||||
|
if (document.getElementById(prefix + '_h'))
|
||||||
|
document.getElementById(prefix + '_h').value = String(h).padStart(2, '0');
|
||||||
|
if (document.getElementById(prefix + '_m'))
|
||||||
|
document.getElementById(prefix + '_m').value = String(m).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tpHtml(prefix, initD, initH, initM) {
|
||||||
|
const d = initD ?? 0, h = initH ?? 0, m = initM ?? 0;
|
||||||
|
return `<div class="time-picker">` +
|
||||||
|
`<div class="tp-seg"><button type="button" onclick="tpChange('${prefix}',-1,'d')">−</button><input type="text" id="${prefix}_d" value="${d}" readonly><button type="button" onclick="tpChange('${prefix}',1,'d')">+</button><span class="tp-label">Tage</span></div>` +
|
||||||
|
`<div class="tp-seg"><button type="button" onclick="tpChange('${prefix}',-1,'h')">−</button><input type="text" id="${prefix}_h" value="${String(h).padStart(2,'0')}" readonly><button type="button" onclick="tpChange('${prefix}',1,'h')">+</button><span class="tp-label">Stunden</span></div>` +
|
||||||
|
`<div class="tp-seg"><button type="button" onclick="tpChange('${prefix}',-1,'m')">−</button><input type="text" id="${prefix}_m" value="${String(m).padStart(2,'0')}" readonly><button type="button" onclick="tpChange('${prefix}',1,'m')">+</button><span class="tp-label">Minuten</span></div>` +
|
||||||
|
`</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tpHtmlDH(prefix, initD, initH) {
|
||||||
|
const d = initD ?? 0, h = initH ?? 0;
|
||||||
|
return `<div class="time-picker">` +
|
||||||
|
`<div class="tp-seg"><button type="button" onclick="tpChange('${prefix}',-1,'d')">−</button><input type="text" id="${prefix}_d" value="${d}" readonly><button type="button" onclick="tpChange('${prefix}',1,'d')">+</button><span class="tp-label">Tage</span></div>` +
|
||||||
|
`<div class="tp-seg"><button type="button" onclick="tpChange('${prefix}',-1,'h')">−</button><input type="text" id="${prefix}_h" value="${String(h).padStart(2,'0')}" readonly><button type="button" onclick="tpChange('${prefix}',1,'h')">+</button><span class="tp-label">Stunden</span></div>` +
|
||||||
|
`</div>`;
|
||||||
|
}
|
||||||
@@ -35,15 +35,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="topbar-right">
|
<div class="topbar-right">
|
||||||
<button class="topbar-btn" id="topbarMsgBtn" title="Nachrichten">
|
<button class="topbar-btn" id="topbarMsgBtn" title="Nachrichten">
|
||||||
${IC('MESSAGES')}
|
<img src="/img/icons/envelope.png" style="width:1.875rem;height:1.875rem;object-fit:contain;display:block;" alt="">
|
||||||
<span class="topbar-badge" id="topbarMsgBadge"></span>
|
<span class="topbar-badge" id="topbarMsgBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="topbar-btn" id="topbarNotifBtn" title="Benachrichtigungen">
|
<button class="topbar-btn" id="topbarNotifBtn" title="Benachrichtigungen">
|
||||||
${IC('NOTIFICATIONS')}
|
<img src="/img/icons/bell.png" style="width:1.875rem;height:1.875rem;object-fit:contain;display:block;" alt="">
|
||||||
<span class="topbar-badge" id="topbarNotifBadge"></span>
|
<span class="topbar-badge" id="topbarNotifBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="topbar-btn" id="topbarInvBtn" title="Einladungen">
|
<button class="topbar-btn" id="topbarInvBtn" title="Einladungen">
|
||||||
${IC('INVITATIONS')}
|
<img src="/img/icons/stars.png" style="width:1.875rem;height:1.875rem;object-fit:contain;display:block;" alt="">
|
||||||
<span class="topbar-badge" id="topbarInvBadge"></span>
|
<span class="topbar-badge" id="topbarInvBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="topbar-btn topbar-profile-btn" id="topbarProfileBtn">
|
<button class="topbar-btn topbar-profile-btn" id="topbarProfileBtn">
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
document.body.insertAdjacentHTML('beforeend', `
|
document.body.insertAdjacentHTML('beforeend', `
|
||||||
<div class="topbar-panel" id="topbarMsgPanel">
|
<div class="topbar-panel" id="topbarMsgPanel">
|
||||||
<div class="topbar-panel-header">
|
<div class="topbar-panel-header">
|
||||||
<span>${IC('MESSAGES')} Nachrichten</span>
|
<span><img src="/img/icons/envelope.png" style="width:1rem;height:1rem;object-fit:contain;vertical-align:middle;margin-right:0.3rem;" alt=""> Nachrichten</span>
|
||||||
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-panel-body" id="topbarMsgBody"></div>
|
<div class="topbar-panel-body" id="topbarMsgBody"></div>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
<div class="topbar-panel" id="topbarNotifPanel">
|
<div class="topbar-panel" id="topbarNotifPanel">
|
||||||
<div class="topbar-panel-header">
|
<div class="topbar-panel-header">
|
||||||
<span>${IC('NOTIFICATIONS')} Benachrichtigungen</span>
|
<span><img src="/img/icons/bell.png" style="width:1rem;height:1rem;object-fit:contain;vertical-align:middle;margin-right:0.3rem;" alt=""> Benachrichtigungen</span>
|
||||||
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-panel-body" id="topbarNotifBody"></div>
|
<div class="topbar-panel-body" id="topbarNotifBody"></div>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
<div class="topbar-panel" id="topbarInvPanel">
|
<div class="topbar-panel" id="topbarInvPanel">
|
||||||
<div class="topbar-panel-header">
|
<div class="topbar-panel-header">
|
||||||
<span>${IC('INVITATIONS')} Einladungen</span>
|
<span><img src="/img/icons/stars.png" style="width:1rem;height:1rem;object-fit:contain;vertical-align:middle;margin-right:0.3rem;" alt=""> Einladungen</span>
|
||||||
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-panel-body" id="topbarInvBody"></div>
|
<div class="topbar-panel-body" id="topbarInvBody"></div>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ public class CardLockController {
|
|||||||
List<CardEnum> initialCards, Integer pickEveryMinute, boolean accumulatePicks, boolean showRemainingCards,
|
List<CardEnum> initialCards, Integer pickEveryMinute, boolean accumulatePicks, boolean showRemainingCards,
|
||||||
LocalDateTime latestOpeningtime, Integer hygineOpeningDurationMinutes, Integer hygineOpeningEveryMinites,
|
LocalDateTime latestOpeningtime, Integer hygineOpeningDurationMinutes, Integer hygineOpeningEveryMinites,
|
||||||
List<Task> tasks, boolean requiresVerification, boolean testLock, Integer unlockCodeLines,
|
List<Task> tasks, boolean requiresVerification, boolean testLock, Integer unlockCodeLines,
|
||||||
TaskMode taskMode, LockControllType controllType) {
|
TaskMode taskMode, LockControllType controllType, UUID gameSetId, Integer gameSpieldauerIdx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final SecureRandom RNG = new SecureRandom();
|
private static final SecureRandom RNG = new SecureRandom();
|
||||||
@@ -176,6 +176,8 @@ public class CardLockController {
|
|||||||
lock.setRequiresVerification(req.requiresVerification());
|
lock.setRequiresVerification(req.requiresVerification());
|
||||||
lock.setTestLock(false);
|
lock.setTestLock(false);
|
||||||
lock.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM);
|
lock.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM);
|
||||||
|
lock.setGameSetId(req.gameSetId());
|
||||||
|
lock.setGameSpieldauerIdx(req.gameSpieldauerIdx());
|
||||||
// startTime, unlockCode, unlockCodeLines left null until lockee accepts
|
// startTime, unlockCode, unlockCodeLines left null until lockee accepts
|
||||||
cardlockRepository.save(lock);
|
cardlockRepository.save(lock);
|
||||||
|
|
||||||
@@ -222,6 +224,8 @@ public class CardLockController {
|
|||||||
lock.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM);
|
lock.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM);
|
||||||
lock.setUnlockCodeLength(codeLines);
|
lock.setUnlockCodeLength(codeLines);
|
||||||
lock.setControllType(controllType);
|
lock.setControllType(controllType);
|
||||||
|
lock.setGameSetId(req.gameSetId());
|
||||||
|
lock.setGameSpieldauerIdx(req.gameSpieldauerIdx());
|
||||||
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
lock.setStartTime(now);
|
lock.setStartTime(now);
|
||||||
@@ -625,6 +629,63 @@ public class CardLockController {
|
|||||||
result.put("slowmoUntil", l.getSlowmoUntil() != null ? l.getSlowmoUntil().toString() : null);
|
result.put("slowmoUntil", l.getSlowmoUntil() != null ? l.getSlowmoUntil().toString() : null);
|
||||||
result.put("speedupUntil", l.getSpeedupUntil() != null ? l.getSpeedupUntil().toString() : null);
|
result.put("speedupUntil", l.getSpeedupUntil() != null ? l.getSpeedupUntil().toString() : null);
|
||||||
|
|
||||||
|
// Game-Card Parking: Frist prüfen und ggf. Strafe anwenden
|
||||||
|
if (l.getGameCardParkedAt() != null) {
|
||||||
|
LocalDateTime deadline = l.getGameCardParkedAt().plusHours(1);
|
||||||
|
if (deadline.isBefore(LocalDateTime.now())) {
|
||||||
|
if (l.getKeyholder() != null) {
|
||||||
|
String meName = userRepository.findById(myId).map(u -> u.getName()).orElse("");
|
||||||
|
sendMessage(myId, l.getKeyholder(),
|
||||||
|
meName + " hat die Spiel-Karte nicht innerhalb einer Stunde gestartet.",
|
||||||
|
"/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
|
||||||
|
} else {
|
||||||
|
LocalDateTime freezeUntil = LocalDateTime.now().plusHours(4);
|
||||||
|
l.setFrozenUntil(freezeUntil);
|
||||||
|
l.setNextCardIn(freezeUntil);
|
||||||
|
result.put("frozenUntill", freezeUntil.toString());
|
||||||
|
result.put("nextCardIn", freezeUntil.toString());
|
||||||
|
}
|
||||||
|
l.setGameCardParkedAt(null);
|
||||||
|
cardlockRepository.save(l);
|
||||||
|
result.put("gameCardParkedAt", null);
|
||||||
|
result.put("gameSetId", null);
|
||||||
|
} else {
|
||||||
|
result.put("gameCardParkedAt", l.getGameCardParkedAt().toString());
|
||||||
|
result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null);
|
||||||
|
result.put("gameCardDeadline", deadline.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.put("gameCardParkedAt", null);
|
||||||
|
result.put("gameSetId", null);
|
||||||
|
}
|
||||||
|
result.put("gameActive", l.isGameActive());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/cardlock/{lockId}/game/start")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Map<String, Object>> startParkedGame(@PathVariable("lockId") UUID lockId, Principal principal) {
|
||||||
|
UUID myId = userService.requireUser(principal).getUserId();
|
||||||
|
|
||||||
|
var lockOpt = cardlockRepository.findById(lockId);
|
||||||
|
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
var l = lockOpt.get();
|
||||||
|
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||||
|
if (l.getGameCardParkedAt() == null) return ResponseEntity.status(409).body(Map.of("error", "Keine geparktes Spiel vorhanden."));
|
||||||
|
|
||||||
|
LocalDateTime deadline = l.getGameCardParkedAt().plusHours(1);
|
||||||
|
if (deadline.isBefore(LocalDateTime.now())) return ResponseEntity.status(409).body(Map.of("error", "Frist abgelaufen."));
|
||||||
|
|
||||||
|
UUID gameSetId = l.getGameSetId();
|
||||||
|
l.setGameCardParkedAt(null);
|
||||||
|
l.setFrozenUntil(null);
|
||||||
|
l.setNextCardIn(LocalDateTime.now().plusMinutes(l.getPickEveryMinute() != null ? l.getPickEveryMinute() : 60));
|
||||||
|
l.setGameActive(true);
|
||||||
|
cardlockRepository.save(l);
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("gameSetId", gameSetId != null ? gameSetId.toString() : null);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,4 +50,16 @@ public class CardLockEntity extends BaseLockEntity {
|
|||||||
private LocalDateTime slowmoUntil;
|
private LocalDateTime slowmoUntil;
|
||||||
@Column
|
@Column
|
||||||
private LocalDateTime speedupUntil;
|
private LocalDateTime speedupUntil;
|
||||||
|
|
||||||
|
// Game-Card Parking
|
||||||
|
@Column
|
||||||
|
private LocalDateTime gameCardParkedAt;
|
||||||
|
@Column
|
||||||
|
private java.util.UUID gameSetId;
|
||||||
|
@Column
|
||||||
|
private Integer gameSpieldauerIdx;
|
||||||
|
|
||||||
|
// Game aktiv (läuft gerade – Lock gesperrt bis Abschluss)
|
||||||
|
@Column(columnDefinition = "TINYINT(1) NOT NULL DEFAULT 0")
|
||||||
|
private boolean gameActive;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
|
|
||||||
public CardDTO getNextCard() {
|
public CardDTO getNextCard() {
|
||||||
LOGGER.debug("New Card requested by user {}", lock.getLockee());
|
LOGGER.debug("New Card requested by user {}", lock.getLockee());
|
||||||
|
if (lock.isGameActive()) return null;
|
||||||
CardDTO card = null;
|
CardDTO card = null;
|
||||||
if (lock.isKeyholderRequestedUnlock()
|
if (lock.isKeyholderRequestedUnlock()
|
||||||
|| (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now()))) {
|
|| (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now()))) {
|
||||||
@@ -285,6 +286,11 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String game() {
|
public String game() {
|
||||||
|
LocalDateTime deadline = LocalDateTime.now().plusHours(1);
|
||||||
|
lock.setGameCardParkedAt(LocalDateTime.now());
|
||||||
|
lock.setFrozenUntil(deadline);
|
||||||
|
lock.setNextCardIn(deadline);
|
||||||
|
lock.setOpenPicks(0);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ public class BaseLockTemplateController {
|
|||||||
dto.put("taskSetId", t.getTaskSetId());
|
dto.put("taskSetId", t.getTaskSetId());
|
||||||
dto.put("requiresVerification", t.isRequiresVerification());
|
dto.put("requiresVerification", t.isRequiresVerification());
|
||||||
dto.put("taskCardMode", t.getTaskCardMode());
|
dto.put("taskCardMode", t.getTaskCardMode());
|
||||||
|
dto.put("gameSetId", t.getGameSetId());
|
||||||
|
dto.put("gameSpieldauerIdx", t.getGameSpieldauerIdx());
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package de.oaa.xxx.games.chastity.common;
|
package de.oaa.xxx.games.chastity.common;
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -15,6 +17,8 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
|
||||||
|
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||||
import de.oaa.xxx.games.common.aufgaben.AufgabenList;
|
import de.oaa.xxx.games.common.aufgaben.AufgabenList;
|
||||||
import de.oaa.xxx.games.common.aufgaben.AvailableIn;
|
import de.oaa.xxx.games.common.aufgaben.AvailableIn;
|
||||||
import de.oaa.xxx.games.common.repository.AufgabeRepository;
|
import de.oaa.xxx.games.common.repository.AufgabeRepository;
|
||||||
@@ -41,6 +45,7 @@ public class LockGameController {
|
|||||||
private final FinisherRepository finisherRepository;
|
private final FinisherRepository finisherRepository;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final CardlockRepository cardlockRepository;
|
||||||
|
|
||||||
public LockGameController(LockGameRepository lockGameRepository,
|
public LockGameController(LockGameRepository lockGameRepository,
|
||||||
LockGameLockRepository lockGameLockRepository,
|
LockGameLockRepository lockGameLockRepository,
|
||||||
@@ -49,7 +54,8 @@ public class LockGameController {
|
|||||||
SperreRepository sperreRepository,
|
SperreRepository sperreRepository,
|
||||||
FinisherRepository finisherRepository,
|
FinisherRepository finisherRepository,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
ObjectMapper objectMapper) {
|
ObjectMapper objectMapper,
|
||||||
|
CardlockRepository cardlockRepository) {
|
||||||
this.lockGameRepository = lockGameRepository;
|
this.lockGameRepository = lockGameRepository;
|
||||||
this.lockGameLockRepository = lockGameLockRepository;
|
this.lockGameLockRepository = lockGameLockRepository;
|
||||||
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
||||||
@@ -58,6 +64,7 @@ public class LockGameController {
|
|||||||
this.finisherRepository = finisherRepository;
|
this.finisherRepository = finisherRepository;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
|
this.cardlockRepository = cardlockRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verfügbare CHASTITY_ONLY-Gruppen des angemeldeten Users. */
|
/** Verfügbare CHASTITY_ONLY-Gruppen des angemeldeten Users. */
|
||||||
@@ -80,6 +87,7 @@ public class LockGameController {
|
|||||||
* Initialisiert (oder startet neu) ein Lock-Game für den angemeldeten User.
|
* Initialisiert (oder startet neu) ein Lock-Game für den angemeldeten User.
|
||||||
* Lädt die angegebene Aufgabengruppe und baut daraus die AufgabenList.
|
* Lädt die angegebene Aufgabengruppe und baut daraus die AufgabenList.
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
@PostMapping("/init")
|
@PostMapping("/init")
|
||||||
public ResponseEntity<?> init(@RequestParam UUID aufgabenGruppeId, Principal principal) {
|
public ResponseEntity<?> init(@RequestParam UUID aufgabenGruppeId, Principal principal) {
|
||||||
UUID userId = userService.requireUser(principal).getUserId();
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
@@ -199,6 +207,30 @@ public class LockGameController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@PostMapping("/complete")
|
||||||
|
public ResponseEntity<?> completeGame(@RequestParam(required = false) UUID lockId, Principal principal) {
|
||||||
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
|
var opt = lockGameRepository.findByUserId(userId);
|
||||||
|
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
LockGameEntity game = opt.get();
|
||||||
|
lockGameLockRepository.deleteAll(lockGameLockRepository.findByGameId(game.getGameId()));
|
||||||
|
lockGameRepository.delete(game);
|
||||||
|
|
||||||
|
if (lockId != null) {
|
||||||
|
cardlockRepository.findById(lockId).ifPresent(l -> {
|
||||||
|
if (l.getLockee().equals(userId)) {
|
||||||
|
l.setGameActive(false);
|
||||||
|
l.setFrozenUntil(null);
|
||||||
|
l.setNextCardIn(LocalDateTime.now()
|
||||||
|
.plusMinutes(l.getPickEveryMinute() != null ? l.getPickEveryMinute() : 60));
|
||||||
|
cardlockRepository.save(l);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
private LockGameService buildService(LockGameEntity entity) throws Exception {
|
private LockGameService buildService(LockGameEntity entity) throws Exception {
|
||||||
return new LockGameService(entity, lockGameRepository, lockGameLockRepository);
|
return new LockGameService(entity, lockGameRepository, lockGameLockRepository);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ public class LockGameEntity {
|
|||||||
private String activeTask;
|
private String activeTask;
|
||||||
@Column
|
@Column
|
||||||
private LocalDateTime activeTaskEnd;
|
private LocalDateTime activeTaskEnd;
|
||||||
@Column
|
@Column(columnDefinition = "TEXT")
|
||||||
private String taskInQueue;
|
private String taskInQueue;
|
||||||
@Column
|
@Column(columnDefinition = "TEXT")
|
||||||
private String lockInQueue;
|
private String lockInQueue;
|
||||||
@Column
|
@Column
|
||||||
private UUID setupId;
|
private UUID setupId;
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ public class LockGameService {
|
|||||||
gamestate.setTaskInQueue(null);
|
gamestate.setTaskInQueue(null);
|
||||||
var time = getAufgabeTime(aufgabe);
|
var time = getAufgabeTime(aufgabe);
|
||||||
gamestate.setActiveTaskEnd(LocalDateTime.now().plusSeconds(time));
|
gamestate.setActiveTaskEnd(LocalDateTime.now().plusSeconds(time));
|
||||||
|
lockGameRepository.save(gamestate);
|
||||||
return time;
|
return time;
|
||||||
} else if (gamestate.getLockInQueue() != null) {
|
} else if (gamestate.getLockInQueue() != null) {
|
||||||
var lock = mapper.readValue(gamestate.getLockInQueue(), Sperre.class);
|
var lock = mapper.readValue(gamestate.getLockInQueue(), Sperre.class);
|
||||||
@@ -121,12 +122,16 @@ public class LockGameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Integer getLockTime(Sperre sperre) {
|
private Integer getLockTime(Sperre sperre) {
|
||||||
var time = new Random().nextInt(sperre.getMinutenVon(), sperre.getMinutenBis());
|
int von = sperre.getMinutenVon() != null ? sperre.getMinutenVon() : 0;
|
||||||
|
int bis = sperre.getMinutenBis() != null ? sperre.getMinutenBis() : von;
|
||||||
|
int time = von < bis ? new Random().nextInt(von, bis) : von;
|
||||||
return (int) (time * gamestate.getZeitfaktorZeitstrafen());
|
return (int) (time * gamestate.getZeitfaktorZeitstrafen());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer getAufgabeTime(Aufgabe aufgabe) {
|
private Integer getAufgabeTime(Aufgabe aufgabe) {
|
||||||
var time = new Random().nextInt(aufgabe.getSekundenVon(), aufgabe.getSekundenBis());
|
int von = aufgabe.getSekundenVon() != null ? aufgabe.getSekundenVon() : 0;
|
||||||
|
int bis = aufgabe.getSekundenBis() != null ? aufgabe.getSekundenBis() : von;
|
||||||
|
int time = von < bis ? new Random().nextInt(von, bis) : von;
|
||||||
return (int) (time * gamestate.getZeitfaktorZeitstrafen());
|
return (int) (time * gamestate.getZeitfaktorZeitstrafen());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- session_id war fälschlicherweise PRIMARY KEY der lock_game-Tabelle.
|
||||||
|
-- Korrekter PK ist game_id (gemäß JPA-Entity @Id).
|
||||||
|
ALTER TABLE lock_game DROP PRIMARY KEY;
|
||||||
|
ALTER TABLE lock_game MODIFY COLUMN session_id VARCHAR(255) NULL;
|
||||||
|
ALTER TABLE lock_game ADD PRIMARY KEY (game_id);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- task_in_queue und lock_in_queue können serialisierte JSON-Objekte speichern,
|
||||||
|
-- die länger als 255 Zeichen sind – auf TEXT erweitern.
|
||||||
|
ALTER TABLE lock_game
|
||||||
|
MODIFY COLUMN task_in_queue TEXT NULL,
|
||||||
|
MODIFY COLUMN lock_in_queue TEXT NULL;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- game_active wurde ohne DEFAULT angelegt; bestehende Zeilen haben NULL –
|
||||||
|
-- auf false setzen und NOT NULL + DEFAULT ergänzen.
|
||||||
|
UPDATE active_lock SET game_active = 0 WHERE game_active IS NULL;
|
||||||
|
ALTER TABLE active_lock
|
||||||
|
MODIFY COLUMN game_active TINYINT(1) NOT NULL DEFAULT 0;
|
||||||
18
src/main/resources/static/css/time-picker.css
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.time-picker { display:flex; align-items:center; gap:0.35rem; flex-wrap:wrap; }
|
||||||
|
.tp-seg { display:flex; flex-direction:column; align-items:center; gap:0; }
|
||||||
|
.tp-seg-row { display:flex; align-items:center; gap:0.2rem; }
|
||||||
|
.tp-seg button {
|
||||||
|
width:24px !important; height:24px !important;
|
||||||
|
background:var(--color-card); border:1px solid var(--color-muted);
|
||||||
|
border-radius:4px; cursor:pointer; font-size:0.9rem; font-weight:700; color:var(--color-text);
|
||||||
|
display:flex; align-items:center; justify-content:center; padding:0; flex-shrink:0;
|
||||||
|
}
|
||||||
|
.tp-seg button:hover { background:var(--color-primary); color:#fff; border-color:var(--color-primary); }
|
||||||
|
.tp-seg .tp-seg-row input {
|
||||||
|
width:28px !important; padding:0.15rem 0 !important;
|
||||||
|
text-align:center; background:var(--color-card);
|
||||||
|
border:1px solid var(--color-muted); border-radius:4px;
|
||||||
|
color:var(--color-text); font-size:0.9rem; font-weight:600;
|
||||||
|
font-family:monospace; box-sizing:border-box;
|
||||||
|
}
|
||||||
|
.tp-seg .tp-label { font-size:0.58rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.03em; line-height:1.2; }
|
||||||
@@ -658,6 +658,23 @@
|
|||||||
<button class="btn-hygiene" id="hygieneBtn" style="display:none;" onclick="openHygieneModal()">🚿 Hygiene-Öffnung</button>
|
<button class="btn-hygiene" id="hygieneBtn" style="display:none;" onclick="openHygieneModal()">🚿 Hygiene-Öffnung</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Geparkte Spiel-Karte -->
|
||||||
|
<div id="gameCardPanel" class="hygiene-panel" style="display:none;border-color:var(--color-primary);">
|
||||||
|
<div>
|
||||||
|
<div class="hygiene-panel-title" style="color:var(--color-primary);">🎮 Spiel-Karte gezogen</div>
|
||||||
|
<div class="hygiene-countdown" id="gameCardCountdown">–</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-hygiene" onclick="startParkedGame()">▶ Spiel starten</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gameActivePanel" class="hygiene-panel" style="display:none;border-color:var(--color-primary);">
|
||||||
|
<div>
|
||||||
|
<div class="hygiene-panel-title" style="color:var(--color-primary);">🎮 Minispiel läuft</div>
|
||||||
|
<div style="font-size:0.82rem;color:var(--color-muted);">Das Lock ist gesperrt, bis das Spiel abgeschlossen ist.</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-hygiene" onclick="goToActiveGame()">▶ Zum Spiel</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Speed-Effekt-Panel -->
|
<!-- Speed-Effekt-Panel -->
|
||||||
<div id="speedPanel" style="display:none;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:12px;padding:0.85rem 1.1rem;gap:0.35rem;">
|
<div id="speedPanel" style="display:none;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:12px;padding:0.85rem 1.1rem;gap:0.35rem;">
|
||||||
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--color-muted);" id="speedPanelTitle">Slow Motion aktiv</div>
|
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--color-muted);" id="speedPanelTitle">Slow Motion aktiv</div>
|
||||||
@@ -909,6 +926,8 @@
|
|||||||
renderKeyholderBar(lock);
|
renderKeyholderBar(lock);
|
||||||
renderAssignedTasks(lock);
|
renderAssignedTasks(lock);
|
||||||
renderNextCardPanel(lock);
|
renderNextCardPanel(lock);
|
||||||
|
renderGameCardPanel(lock);
|
||||||
|
renderGameActivePanel(lock);
|
||||||
renderHygienePanel(lock);
|
renderHygienePanel(lock);
|
||||||
renderSpeedPanel(lock);
|
renderSpeedPanel(lock);
|
||||||
renderVerificationPanel(lock);
|
renderVerificationPanel(lock);
|
||||||
@@ -1170,6 +1189,9 @@
|
|||||||
const panel = document.getElementById('nextcardPanel');
|
const panel = document.getElementById('nextcardPanel');
|
||||||
const cardsDiv = document.getElementById('nextcardCards');
|
const cardsDiv = document.getElementById('nextcardCards');
|
||||||
const overlay = document.getElementById('nextcardOverlay');
|
const overlay = document.getElementById('nextcardOverlay');
|
||||||
|
|
||||||
|
if (lock.gameActive) { panel.style.display = 'none'; return; }
|
||||||
|
|
||||||
panel.style.display = '';
|
panel.style.display = '';
|
||||||
panel.classList.remove('drawable');
|
panel.classList.remove('drawable');
|
||||||
|
|
||||||
@@ -1286,6 +1308,45 @@
|
|||||||
tickInterval = setInterval(tick, 1000);
|
tickInterval = setInterval(tick, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let gameCardPanelTick = null;
|
||||||
|
|
||||||
|
function renderGameCardPanel(lock) {
|
||||||
|
if (gameCardPanelTick) { clearInterval(gameCardPanelTick); gameCardPanelTick = null; }
|
||||||
|
const panel = document.getElementById('gameCardPanel');
|
||||||
|
if (!lock.gameCardParkedAt || !lock.gameCardDeadline) {
|
||||||
|
panel.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
panel.style.display = '';
|
||||||
|
const deadline = new Date(lock.gameCardDeadline);
|
||||||
|
const cdEl = document.getElementById('gameCardCountdown');
|
||||||
|
function tick() {
|
||||||
|
const diff = deadline - Date.now();
|
||||||
|
if (diff <= 0) { panel.style.display = 'none'; clearInterval(gameCardPanelTick); gameCardPanelTick = null; return; }
|
||||||
|
cdEl.textContent = fmtCountdown(diff);
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
gameCardPanelTick = setInterval(tick, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startParkedGame() {
|
||||||
|
const res = await fetch('/keyholder/cardlock/' + lockId + '/game/start', { method: 'POST' });
|
||||||
|
if (!res.ok) { alert('Fehler beim Starten des Spiels.'); return; }
|
||||||
|
const data = await res.json();
|
||||||
|
const gameSetId = data.gameSetId;
|
||||||
|
const url = '/games/chastity/taskgame.html?lockId=' + lockId
|
||||||
|
+ (gameSetId ? '&gameSetId=' + gameSetId : '');
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGameActivePanel(lock) {
|
||||||
|
document.getElementById('gameActivePanel').style.display = lock.gameActive ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToActiveGame() {
|
||||||
|
window.location.href = '/games/chastity/taskgame.html?lockId=' + lockId;
|
||||||
|
}
|
||||||
|
|
||||||
function renderHygienePanel(lock) {
|
function renderHygienePanel(lock) {
|
||||||
if (hygienePanelTick) { clearInterval(hygienePanelTick); hygienePanelTick = null; }
|
if (hygienePanelTick) { clearInterval(hygienePanelTick); hygienePanelTick = null; }
|
||||||
if (!lock.hygieneEnabled) return;
|
if (!lock.hygieneEnabled) return;
|
||||||
@@ -1714,10 +1775,8 @@
|
|||||||
|
|
||||||
if (dto.card === 'GAME_CARD') {
|
if (dto.card === 'GAME_CARD') {
|
||||||
const btn = document.getElementById('btnDrawOk');
|
const btn = document.getElementById('btnDrawOk');
|
||||||
btn.textContent = '▶ Spiel starten';
|
btn.textContent = 'OK';
|
||||||
btn.onclick = function() {
|
btn.onclick = closeDrawModal;
|
||||||
window.location.href = '/games/chastity/taskgame.html?lockId=' + lockId;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, 700);
|
}, 700);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>Keyholder – xXx Sphere</title>
|
<title>Keyholder – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/time-picker.css">
|
||||||
<style>
|
<style>
|
||||||
.lock-list { display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem; }
|
.lock-list { display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem; }
|
||||||
|
|
||||||
@@ -80,27 +81,6 @@
|
|||||||
.violation-item:last-child { border-bottom:none; }
|
.violation-item:last-child { border-bottom:none; }
|
||||||
|
|
||||||
/* Zeitpicker (für Freeze-Dialog) */
|
/* Zeitpicker (für Freeze-Dialog) */
|
||||||
.time-picker { display:flex; align-items:center; gap:0.5rem; }
|
|
||||||
.tp-seg { display:flex; flex-direction:column; align-items:center; gap:0.2rem; }
|
|
||||||
.tp-seg-row { display:flex; align-items:center; gap:0.25rem; }
|
|
||||||
.tp-seg button {
|
|
||||||
width:26px; height:26px;
|
|
||||||
background:var(--color-secondary); border:1px solid var(--color-muted);
|
|
||||||
border-radius:5px; cursor:pointer; font-size:1rem; font-weight:700;
|
|
||||||
color:var(--color-text); display:flex; align-items:center; justify-content:center;
|
|
||||||
padding:0; flex-shrink:0;
|
|
||||||
}
|
|
||||||
.tp-seg button:hover { background:var(--color-primary); color:#fff; border-color:var(--color-primary); }
|
|
||||||
.tp-seg input {
|
|
||||||
width:30px; text-align:center;
|
|
||||||
background:var(--color-secondary); border:1px solid var(--color-muted);
|
|
||||||
border-radius:4px; color:var(--color-text);
|
|
||||||
font-size:0.95rem; font-weight:600; font-family:monospace;
|
|
||||||
padding:0.18rem 0; box-sizing:border-box;
|
|
||||||
}
|
|
||||||
.tp-seg .tp-label { font-size:0.65rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; }
|
|
||||||
.tp-colon { font-size:1.1rem; font-weight:700; color:var(--color-muted); margin-bottom:1rem; }
|
|
||||||
|
|
||||||
/* ── Tab-Navigation ── */
|
/* ── Tab-Navigation ── */
|
||||||
.kh-tabs { display:flex; gap:0; border-bottom:2px solid var(--color-secondary); margin-bottom:1.25rem; }
|
.kh-tabs { display:flex; gap:0; border-bottom:2px solid var(--color-secondary); margin-bottom:1.25rem; }
|
||||||
.kh-tab {
|
.kh-tab {
|
||||||
@@ -311,32 +291,9 @@
|
|||||||
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.4rem;">Annahme-Frist</div>
|
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.4rem;">Annahme-Frist</div>
|
||||||
|
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('at',-1,'d')">−</button><input type="text" id="at_d" value="0" readonly><button type="button" onclick="tpChange('at',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-seg-row">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('at',-1,'h')">−</button><input type="text" id="at_h" value="01" readonly><button type="button" onclick="tpChange('at',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<button type="button" onclick="atTpChange(-1,'d')">−</button>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('at',-1,'m')">−</button><input type="text" id="at_m" value="00" readonly><button type="button" onclick="tpChange('at',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<input type="text" id="at_d" value="0" readonly>
|
|
||||||
<button type="button" onclick="atTpChange(1,'d')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Tage</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="atTpChange(-1,'h')">−</button>
|
|
||||||
<input type="text" id="at_h" value="01" readonly>
|
|
||||||
<button type="button" onclick="atTpChange(1,'h')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Stunden</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="atTpChange(-1,'m')">−</button>
|
|
||||||
<input type="text" id="at_m" value="00" readonly>
|
|
||||||
<button type="button" onclick="atTpChange(1,'m')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Minuten</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -350,32 +307,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<div id="atPenaltyFreezeFields" style="padding-left:1.6rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
|
<div id="atPenaltyFreezeFields" style="padding-left:1.6rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('atf',-1,'d')">−</button><input type="text" id="atf_d" value="0" readonly><button type="button" onclick="tpChange('atf',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-seg-row">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('atf',-1,'h')">−</button><input type="text" id="atf_h" value="04" readonly><button type="button" onclick="tpChange('atf',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<button type="button" onclick="atFreezeTpChange(-1,'d')">−</button>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('atf',-1,'m')">−</button><input type="text" id="atf_m" value="00" readonly><button type="button" onclick="tpChange('atf',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<input type="text" id="atf_d" value="0" readonly>
|
|
||||||
<button type="button" onclick="atFreezeTpChange(1,'d')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Tage</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="atFreezeTpChange(-1,'h')">−</button>
|
|
||||||
<input type="text" id="atf_h" value="04" readonly>
|
|
||||||
<button type="button" onclick="atFreezeTpChange(1,'h')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Stunden</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="atFreezeTpChange(-1,'m')">−</button>
|
|
||||||
<input type="text" id="atf_m" value="00" readonly>
|
|
||||||
<button type="button" onclick="atFreezeTpChange(1,'m')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Minuten</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
|
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
|
||||||
@@ -409,32 +343,9 @@
|
|||||||
<strong style="color:var(--color-text);">Hinweis:</strong> Während des Einfrierens können keine weiteren Karten gezogen werden.
|
<strong style="color:var(--color-text);">Hinweis:</strong> Während des Einfrierens können keine weiteren Karten gezogen werden.
|
||||||
</p>
|
</p>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('freeze',-1,'d')">−</button><input type="text" id="freeze_d" value="0" readonly><button type="button" onclick="tpChange('freeze',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-seg-row">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('freeze',-1,'h')">−</button><input type="text" id="freeze_h" value="04" readonly><button type="button" onclick="tpChange('freeze',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<button type="button" onclick="freezeTpChange(-1,'d')">−</button>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('freeze',-1,'m')">−</button><input type="text" id="freeze_m" value="00" readonly><button type="button" onclick="tpChange('freeze',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<input type="text" id="freeze_d" value="0" readonly>
|
|
||||||
<button type="button" onclick="freezeTpChange(1,'d')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Tage</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="freezeTpChange(-1,'h')">−</button>
|
|
||||||
<input type="text" id="freeze_h" value="04" readonly>
|
|
||||||
<button type="button" onclick="freezeTpChange(1,'h')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Stunden</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="freezeTpChange(-1,'m')">−</button>
|
|
||||||
<input type="text" id="freeze_m" value="00" readonly>
|
|
||||||
<button type="button" onclick="freezeTpChange(1,'m')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Minuten</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="freezeModalError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
|
<div id="freezeModalError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
|
||||||
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
|
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
|
||||||
@@ -462,8 +373,9 @@
|
|||||||
<script src="/js/card-defs.js"></script>
|
<script src="/js/card-defs.js"></script>
|
||||||
<script src="/js/card-display.js"></script>
|
<script src="/js/card-display.js"></script>
|
||||||
<script src="/js/icons.js"></script>
|
<script src="/js/icons.js"></script>
|
||||||
<script src="/js/nav.js"></script>
|
<script src="/js/nav.js"></script>
|
||||||
<script src="/js/social-sidebar.js"></script>
|
<script src="/js/social-sidebar.js"></script>
|
||||||
|
<script src="/js/time-picker.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||||
|
|
||||||
@@ -1095,41 +1007,6 @@
|
|||||||
let assignTaskLockId = null;
|
let assignTaskLockId = null;
|
||||||
let assignTaskSelectedIdx = null;
|
let assignTaskSelectedIdx = null;
|
||||||
|
|
||||||
function atTpChange(delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById('at_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById('at_h').value) || 0;
|
|
||||||
let m = parseInt(document.getElementById('at_m').value) || 0;
|
|
||||||
if (seg === 'm') m += delta; else if (seg === 'h') h += delta; else d += delta;
|
|
||||||
if (m >= 60) { h += Math.floor(m/60); m = m%60; }
|
|
||||||
if (m < 0) { const b = Math.ceil(-m/60); h -= b; m += b*60; }
|
|
||||||
if (h >= 24) { d += Math.floor(h/24); h = h%24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById('at_d').value = d;
|
|
||||||
document.getElementById('at_h').value = String(h).padStart(2,'0');
|
|
||||||
document.getElementById('at_m').value = String(m).padStart(2,'0');
|
|
||||||
}
|
|
||||||
|
|
||||||
function atFreezeTpChange(delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById('atf_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById('atf_h').value) || 0;
|
|
||||||
let m = parseInt(document.getElementById('atf_m').value) || 0;
|
|
||||||
if (seg === 'm') m += delta; else if (seg === 'h') h += delta; else d += delta;
|
|
||||||
if (m >= 60) { h += Math.floor(m/60); m = m%60; }
|
|
||||||
if (m < 0) { const b = Math.ceil(-m/60); h -= b; m += b*60; }
|
|
||||||
if (h >= 24) { d += Math.floor(h/24); h = h%24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById('atf_d').value = d;
|
|
||||||
document.getElementById('atf_h').value = String(h).padStart(2,'0');
|
|
||||||
document.getElementById('atf_m').value = String(m).padStart(2,'0');
|
|
||||||
}
|
|
||||||
|
|
||||||
function atTpToMinutes(prefix) {
|
|
||||||
return (parseInt(document.getElementById(prefix+'_d').value)||0) * 24*60
|
|
||||||
+ (parseInt(document.getElementById(prefix+'_h').value)||0) * 60
|
|
||||||
+ (parseInt(document.getElementById(prefix+'_m').value)||0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function atRedChange(delta) {
|
function atRedChange(delta) {
|
||||||
const inp = document.getElementById('atRedCount');
|
const inp = document.getElementById('atRedCount');
|
||||||
@@ -1273,7 +1150,7 @@
|
|||||||
errEl.style.display = '';
|
errEl.style.display = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const deadlineMinutes = atTpToMinutes('at');
|
const deadlineMinutes = tpToMinutes('at');
|
||||||
if (deadlineMinutes < 1) {
|
if (deadlineMinutes < 1) {
|
||||||
errEl.textContent = 'Bitte eine Annahme-Frist angeben.';
|
errEl.textContent = 'Bitte eine Annahme-Frist angeben.';
|
||||||
errEl.style.display = '';
|
errEl.style.display = '';
|
||||||
@@ -1286,7 +1163,7 @@
|
|||||||
errEl.style.display = '';
|
errEl.style.display = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const penaltyFreezeMinutes = freezeEnabled ? atTpToMinutes('atf') : null;
|
const penaltyFreezeMinutes = freezeEnabled ? tpToMinutes('atf') : null;
|
||||||
const penaltyRedCards = redEnabled ? parseInt(document.getElementById('atRedCount').value) || 1 : null;
|
const penaltyRedCards = redEnabled ? parseInt(document.getElementById('atRedCount').value) || 1 : null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1363,31 +1240,6 @@
|
|||||||
let freezeTargetLockId = null;
|
let freezeTargetLockId = null;
|
||||||
let unfreezeTargetLockId = null;
|
let unfreezeTargetLockId = null;
|
||||||
|
|
||||||
// Zeitpicker-Logik (Dauer)
|
|
||||||
function freezeTpChange(delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById('freeze_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById('freeze_h').value) || 0;
|
|
||||||
let m = parseInt(document.getElementById('freeze_m').value) || 0;
|
|
||||||
if (seg === 'm') m += delta;
|
|
||||||
else if (seg === 'h') h += delta;
|
|
||||||
else if (seg === 'd') d += delta;
|
|
||||||
if (m >= 60) { h += Math.floor(m / 60); m = m % 60; }
|
|
||||||
if (m < 0) { const b = Math.ceil(-m / 60); h -= b; m += b * 60; }
|
|
||||||
if (h >= 24) { d += Math.floor(h / 24); h = h % 24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h / 24); d -= b; h += b * 24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById('freeze_d').value = d;
|
|
||||||
document.getElementById('freeze_h').value = String(h).padStart(2, '0');
|
|
||||||
document.getElementById('freeze_m').value = String(m).padStart(2, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
function freezeTpToMinutes() {
|
|
||||||
const d = parseInt(document.getElementById('freeze_d').value) || 0;
|
|
||||||
const h = parseInt(document.getElementById('freeze_h').value) || 0;
|
|
||||||
const m = parseInt(document.getElementById('freeze_m').value) || 0;
|
|
||||||
return d * 24 * 60 + h * 60 + m;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openFreezeModal(lockId) {
|
function openFreezeModal(lockId) {
|
||||||
freezeTargetLockId = lockId;
|
freezeTargetLockId = lockId;
|
||||||
// Default: 4h
|
// Default: 4h
|
||||||
@@ -1405,7 +1257,7 @@
|
|||||||
|
|
||||||
async function submitFreeze() {
|
async function submitFreeze() {
|
||||||
const lockId = freezeTargetLockId;
|
const lockId = freezeTargetLockId;
|
||||||
const minutes = freezeTpToMinutes();
|
const minutes = tpToMinutes('freeze');
|
||||||
const errEl = document.getElementById('freezeModalError');
|
const errEl = document.getElementById('freezeModalError');
|
||||||
if (minutes < 1) {
|
if (minutes < 1) {
|
||||||
errEl.textContent = 'Bitte eine Dauer von mindestens 1 Minute angeben.';
|
errEl.textContent = 'Bitte eine Dauer von mindestens 1 Minute angeben.';
|
||||||
@@ -1485,26 +1337,6 @@
|
|||||||
let _pendingTaskIndex = null;
|
let _pendingTaskIndex = null;
|
||||||
let _pendingLockId = null;
|
let _pendingLockId = null;
|
||||||
|
|
||||||
function ctpFreezeTpChange(delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById('ctpf_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById('ctpf_h').value) || 0;
|
|
||||||
let m = parseInt(document.getElementById('ctpf_m').value) || 0;
|
|
||||||
if (seg === 'm') m += delta; else if (seg === 'h') h += delta; else d += delta;
|
|
||||||
if (m >= 60) { h += Math.floor(m/60); m = m%60; }
|
|
||||||
if (m < 0) { const b = Math.ceil(-m/60); h -= b; m += b*60; }
|
|
||||||
if (h >= 24) { d += Math.floor(h/24); h = h%24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById('ctpf_d').value = d;
|
|
||||||
document.getElementById('ctpf_h').value = String(h).padStart(2,'0');
|
|
||||||
document.getElementById('ctpf_m').value = String(m).padStart(2,'0');
|
|
||||||
}
|
|
||||||
|
|
||||||
function ctpFreezeToMinutes() {
|
|
||||||
return (parseInt(document.getElementById('ctpf_d').value)||0) * 24*60
|
|
||||||
+ (parseInt(document.getElementById('ctpf_h').value)||0) * 60
|
|
||||||
+ (parseInt(document.getElementById('ctpf_m').value)||0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ctpRedChange(delta) {
|
function ctpRedChange(delta) {
|
||||||
const inp = document.getElementById('ctpRedCount');
|
const inp = document.getElementById('ctpRedCount');
|
||||||
@@ -1566,7 +1398,7 @@
|
|||||||
}
|
}
|
||||||
document.getElementById('ctpPenaltyError').style.display = 'none';
|
document.getElementById('ctpPenaltyError').style.display = 'none';
|
||||||
|
|
||||||
const freezeVal = freezeEnabled ? (ctpFreezeToMinutes() || null) : null;
|
const freezeVal = freezeEnabled ? (tpToMinutes('ctpf') || null) : null;
|
||||||
const redVal = redEnabled ? (parseInt(document.getElementById('ctpRedCount').value) || null) : null;
|
const redVal = redEnabled ? (parseInt(document.getElementById('ctpRedCount').value) || null) : null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@@ -1865,32 +1697,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<div id="ctpPenaltyFreezeFields" style="padding-left:1.6rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
|
<div id="ctpPenaltyFreezeFields" style="padding-left:1.6rem;opacity:0.4;pointer-events:none;transition:opacity 0.15s;">
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('ctpf',-1,'d')">−</button><input type="text" id="ctpf_d" value="0" readonly><button type="button" onclick="tpChange('ctpf',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-seg-row">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('ctpf',-1,'h')">−</button><input type="text" id="ctpf_h" value="04" readonly><button type="button" onclick="tpChange('ctpf',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<button type="button" onclick="ctpFreezeTpChange(-1,'d')">−</button>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('ctpf',-1,'m')">−</button><input type="text" id="ctpf_m" value="00" readonly><button type="button" onclick="tpChange('ctpf',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<input type="text" id="ctpf_d" value="0" readonly>
|
|
||||||
<button type="button" onclick="ctpFreezeTpChange(1,'d')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Tage</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="ctpFreezeTpChange(-1,'h')">−</button>
|
|
||||||
<input type="text" id="ctpf_h" value="04" readonly>
|
|
||||||
<button type="button" onclick="ctpFreezeTpChange(1,'h')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Stunden</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-colon">:</span>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="ctpFreezeTpChange(-1,'m')">−</button>
|
|
||||||
<input type="text" id="ctpf_m" value="00" readonly>
|
|
||||||
<button type="button" onclick="ctpFreezeTpChange(1,'m')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Minuten</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
|
<label style="display:flex;align-items:center;gap:0.6rem;font-size:0.88rem;cursor:pointer;">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>Meine Vorlagen – xXx Sphere</title>
|
<title>Meine Vorlagen – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/time-picker.css">
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
/* ── Liste ── */
|
/* ── Liste ── */
|
||||||
@@ -133,26 +134,6 @@
|
|||||||
}
|
}
|
||||||
.stepper input[type="text"]:focus { outline:none; background:rgba(255,255,255,0.08); }
|
.stepper input[type="text"]:focus { outline:none; background:rgba(255,255,255,0.08); }
|
||||||
|
|
||||||
/* ── Zeitpicker ── */
|
|
||||||
.time-picker { display:flex; align-items:center; gap:0.4rem; flex-wrap:wrap; }
|
|
||||||
.tp-seg { display:flex; flex-direction:column; align-items:center; gap:0.15rem; }
|
|
||||||
.tp-seg-row { display:flex; align-items:center; gap:0.2rem; }
|
|
||||||
.tp-seg button {
|
|
||||||
width:24px; height:24px; background:var(--color-card);
|
|
||||||
border:1px solid var(--color-muted); border-radius:4px;
|
|
||||||
cursor:pointer; font-size:0.9rem; font-weight:700; color:var(--color-text);
|
|
||||||
display:flex; align-items:center; justify-content:center; padding:0; flex-shrink:0;
|
|
||||||
}
|
|
||||||
.tp-seg button:hover { background:var(--color-primary); color:#fff; border-color:var(--color-primary); }
|
|
||||||
.tp-seg input {
|
|
||||||
width:28px; text-align:center; background:var(--color-card);
|
|
||||||
border:1px solid var(--color-muted); border-radius:4px;
|
|
||||||
color:var(--color-text); font-size:0.9rem; font-weight:600;
|
|
||||||
font-family:monospace; padding:0.15rem 0; box-sizing:border-box;
|
|
||||||
}
|
|
||||||
.tp-seg .tp-label { font-size:0.62rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; }
|
|
||||||
.tp-colon { font-size:1rem; font-weight:700; color:var(--color-muted); margin-bottom:0.9rem; }
|
|
||||||
|
|
||||||
/* ── Aufgaben ── */
|
/* ── Aufgaben ── */
|
||||||
.task-list { display:flex; flex-direction:column; gap:0.5rem; margin-bottom:0.6rem; }
|
.task-list { display:flex; flex-direction:column; gap:0.5rem; margin-bottom:0.6rem; }
|
||||||
.task-item {
|
.task-item {
|
||||||
@@ -380,32 +361,9 @@
|
|||||||
<div class="form-row" style="margin-top:0.75rem;">
|
<div class="form-row" style="margin-top:0.75rem;">
|
||||||
<label>Karte ziehen alle</label>
|
<label>Karte ziehen alle</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pe',-1,'d')">−</button><input type="text" id="pe_d" value="0" readonly><button type="button" onclick="tpChange('pe',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-seg-row">
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pe',-1,'h')">−</button><input type="text" id="pe_h" value="01" readonly><button type="button" onclick="tpChange('pe',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<button type="button" onclick="tpChange('pe',-1,'d')">−</button>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pe',-1,'m')">−</button><input type="text" id="pe_m" value="00" readonly><button type="button" onclick="tpChange('pe',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<input type="text" id="pe_d" value="0" readonly>
|
|
||||||
<button type="button" onclick="tpChange('pe',1,'d')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Tage</span>
|
|
||||||
</div>
|
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="tpChange('pe',-1,'h')">−</button>
|
|
||||||
<input type="text" id="pe_h" value="01" readonly>
|
|
||||||
<button type="button" onclick="tpChange('pe',1,'h')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Std</span>
|
|
||||||
</div>
|
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg">
|
|
||||||
<div class="tp-seg-row">
|
|
||||||
<button type="button" onclick="tpChange('pe',-1,'m')">−</button>
|
|
||||||
<input type="text" id="pe_m" value="00" readonly>
|
|
||||||
<button type="button" onclick="tpChange('pe',1,'m')">+</button>
|
|
||||||
</div>
|
|
||||||
<span class="tp-label">Min</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -418,55 +376,53 @@
|
|||||||
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
|
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Aufgaben (CardLock) – nur sichtbar wenn TASK-Karte > 0 -->
|
<!-- Aufgaben-Set – sichtbar wenn TASK > 0 oder GAME_CARD > 0 -->
|
||||||
<div id="sectionCardTasks" style="display:none;">
|
<div id="sectionAufgabenSet" style="display:none;">
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="form-section-title">Aufgaben</div>
|
<div class="form-section-title">Aufgaben-Set</div>
|
||||||
<div style="margin-bottom:0.65rem;">
|
|
||||||
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgabe?</div>
|
|
||||||
<div class="radio-group">
|
|
||||||
<label><input type="radio" name="modalCardTaskMode" value="RANDOM" checked> Zufall</label>
|
|
||||||
<label><input type="radio" name="modalCardTaskMode" value="KEYHOLDER" > Keyholder*In</label>
|
|
||||||
<label><input type="radio" name="modalCardTaskMode" value="COMMUNITY" > Community</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-row" style="margin-bottom:0.5rem;">
|
|
||||||
<label>Aufgaben-Set <span class="required-star">*</span></label>
|
|
||||||
<select id="fCardTaskSetId" onchange="onTaskSetChange('card')">
|
|
||||||
<option value="">Kein Aufgaben-Set</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button class="btn-add" type="button" onclick="openTaskSetModal(null,'card')">+ Neues Set anlegen</button>
|
|
||||||
<div id="cardTaskSetPreview" class="task-set-preview"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Spiel-Karte – nur sichtbar wenn GAME_CARD > 0 -->
|
<!-- Task-Karte: Entscheider + internes Set -->
|
||||||
<div id="sectionGameSetConfig" style="display:none;">
|
<div id="subCardTaskSet" style="display:none;">
|
||||||
<div class="form-section">
|
<div style="margin-bottom:0.65rem;">
|
||||||
<div class="form-section-title">Spiel-Karte – Minispiel</div>
|
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgabe?</div>
|
||||||
<p style="font-size:0.82rem;color:var(--color-muted);margin:0 0 0.75rem;">Wenn eine Spiel-Karte gezogen wird, startet ein Minispiel mit dem gewählten Chastity-Aufgaben-Set.</p>
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="modalCardTaskMode" value="RANDOM" checked> Zufall</label>
|
||||||
<div class="form-row">
|
<label><input type="radio" name="modalCardTaskMode" value="KEYHOLDER" > Keyholder*In</label>
|
||||||
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
|
<label><input type="radio" name="modalCardTaskMode" value="COMMUNITY" > Community</label>
|
||||||
<div style="position:relative;">
|
</div>
|
||||||
<input type="text" id="gameSetSearch" placeholder="Name eingeben zum Suchen…"
|
|
||||||
autocomplete="off" oninput="onGameSetSearch(this.value)" onfocus="onGameSetSearchFocus()">
|
|
||||||
<div id="gameSetDropdown" class="gs-dropdown"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="gameSetSelected" class="gs-selected" style="display:none;"></div>
|
<div class="form-row" style="margin-bottom:0.5rem;">
|
||||||
<input type="hidden" id="fGameSetId">
|
<label>Aufgaben-Set <span class="required-star">*</span></label>
|
||||||
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
|
<select id="fCardTaskSetId" onchange="onTaskSetChange('card')">
|
||||||
|
<option value="">Kein Aufgaben-Set</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn-add" type="button" onclick="openTaskSetModal(null,'card')">+ Neues Set anlegen</button>
|
||||||
|
<div id="cardTaskSetPreview" class="task-set-preview"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row" style="margin-bottom:0;">
|
<!-- Spiel-Karte: Chastity-Set + Spieldauer -->
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.4rem;">
|
<div id="subGameSet" style="display:none;">
|
||||||
<label for="sldGameSpieldauer" style="margin:0;">Spieldauer</label>
|
<div class="form-row">
|
||||||
<span id="valGameSpieldauer" style="font-size:0.85rem;color:var(--color-muted);">Mittel</span>
|
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
|
||||||
|
<div style="position:relative;">
|
||||||
|
<input type="text" id="gameSetSearch" placeholder="Name eingeben zum Suchen…"
|
||||||
|
autocomplete="off" oninput="onGameSetSearch(this.value)" onfocus="onGameSetSearchFocus()">
|
||||||
|
<div id="gameSetDropdown" class="gs-dropdown"></div>
|
||||||
|
</div>
|
||||||
|
<div id="gameSetSelected" class="gs-selected" style="display:none;"></div>
|
||||||
|
<input type="hidden" id="fGameSetId">
|
||||||
|
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.4rem;">
|
||||||
|
<label for="sldGameSpieldauer" style="margin:0;">Spieldauer</label>
|
||||||
|
<span id="valGameSpieldauer" style="font-size:0.85rem;color:var(--color-muted);">Mittel</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2"
|
||||||
|
oninput="updateGameSpieldauer(this.value)"
|
||||||
|
style="width:100%;accent-color:var(--color-primary);">
|
||||||
</div>
|
</div>
|
||||||
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2"
|
|
||||||
oninput="updateGameSpieldauer(this.value)"
|
|
||||||
style="width:100%;accent-color:var(--color-primary);">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -482,20 +438,16 @@
|
|||||||
<label>Mindestdauer (optional)</label>
|
<label>Mindestdauer (optional)</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'d')">−</button><input type="text" id="tmin_d" value="0" readonly><button type="button" onclick="tpChange('tmin',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('tmin',-1,'d')">−</button><input type="text" id="tmin_d" value="0" readonly><button type="button" onclick="tpChange('tmin',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'h')">−</button><input type="text" id="tmin_h" value="00" readonly><button type="button" onclick="tpChange('tmin',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'h')">−</button><input type="text" id="tmin_h" value="00" readonly><button type="button" onclick="tpChange('tmin',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'m')">−</button><input type="text" id="tmin_m" value="00" readonly><button type="button" onclick="tpChange('tmin',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmin',-1,'m')">−</button><input type="text" id="tmin_m" value="00" readonly><button type="button" onclick="tpChange('tmin',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" id="rowMaxTime">
|
<div class="form-row" id="rowMaxTime">
|
||||||
<label>Maximaldauer<span class="required-star">*</span></label>
|
<label>Maximaldauer<span class="required-star">*</span></label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'d')">−</button><input type="text" id="tmax_d" value="0" readonly><button type="button" onclick="tpChange('tmax',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('tmax',-1,'d')">−</button><input type="text" id="tmax_d" value="0" readonly><button type="button" onclick="tpChange('tmax',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'h')">−</button><input type="text" id="tmax_h" value="01" readonly><button type="button" onclick="tpChange('tmax',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'h')">−</button><input type="text" id="tmax_h" value="01" readonly><button type="button" onclick="tpChange('tmax',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'m')">−</button><input type="text" id="tmax_m" value="00" readonly><button type="button" onclick="tpChange('tmax',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('tmax',-1,'m')">−</button><input type="text" id="tmax_m" value="00" readonly><button type="button" onclick="tpChange('tmax',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox-row">
|
<div class="checkbox-row">
|
||||||
@@ -516,10 +468,8 @@
|
|||||||
<label>Rad drehen alle</label>
|
<label>Rad drehen alle</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'d')">−</button><input type="text" id="se_d" value="0" readonly><button type="button" onclick="tpChange('se',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('se',-1,'d')">−</button><input type="text" id="se_d" value="0" readonly><button type="button" onclick="tpChange('se',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'h')">−</button><input type="text" id="se_h" value="01" readonly><button type="button" onclick="tpChange('se',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'h')">−</button><input type="text" id="se_h" value="01" readonly><button type="button" onclick="tpChange('se',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'m')">−</button><input type="text" id="se_m" value="00" readonly><button type="button" onclick="tpChange('se',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'m')">−</button><input type="text" id="se_m" value="00" readonly><button type="button" onclick="tpChange('se',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
@@ -545,10 +495,8 @@
|
|||||||
<label>Aufgaben alle</label>
|
<label>Aufgaben alle</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'d')">−</button><input type="text" id="te_d" value="0" readonly><button type="button" onclick="tpChange('te',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('te',-1,'d')">−</button><input type="text" id="te_d" value="0" readonly><button type="button" onclick="tpChange('te',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'h')">−</button><input type="text" id="te_h" value="08" readonly><button type="button" onclick="tpChange('te',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'h')">−</button><input type="text" id="te_h" value="08" readonly><button type="button" onclick="tpChange('te',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'m')">−</button><input type="text" id="te_m" value="00" readonly><button type="button" onclick="tpChange('te',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'m')">−</button><input type="text" id="te_m" value="00" readonly><button type="button" onclick="tpChange('te',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="margin-bottom:0;">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
@@ -592,10 +540,8 @@
|
|||||||
<label>Dauer</label>
|
<label>Dauer</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'d')">−</button><input type="text" id="pv_d" value="0" readonly><button type="button" onclick="tpChange('pv',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('pv',-1,'d')">−</button><input type="text" id="pv_d" value="0" readonly><button type="button" onclick="tpChange('pv',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'h')">−</button><input type="text" id="pv_h" value="01" readonly><button type="button" onclick="tpChange('pv',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'h')">−</button><input type="text" id="pv_h" value="01" readonly><button type="button" onclick="tpChange('pv',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'m')">−</button><input type="text" id="pv_m" value="00" readonly><button type="button" onclick="tpChange('pv',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'m')">−</button><input type="text" id="pv_m" value="00" readonly><button type="button" onclick="tpChange('pv',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -613,20 +559,16 @@
|
|||||||
<label>Hygiene-Öffnung alle</label>
|
<label>Hygiene-Öffnung alle</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'d')">−</button><input type="text" id="he_d" value="1" readonly><button type="button" onclick="tpChange('he',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('he',-1,'d')">−</button><input type="text" id="he_d" value="1" readonly><button type="button" onclick="tpChange('he',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'h')">−</button><input type="text" id="he_h" value="00" readonly><button type="button" onclick="tpChange('he',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'h')">−</button><input type="text" id="he_h" value="00" readonly><button type="button" onclick="tpChange('he',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'m')">−</button><input type="text" id="he_m" value="00" readonly><button type="button" onclick="tpChange('he',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('he',-1,'m')">−</button><input type="text" id="he_m" value="00" readonly><button type="button" onclick="tpChange('he',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="margin-bottom:0;">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
<label>Dauer der Öffnung</label>
|
<label>Dauer der Öffnung</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'d')">−</button><input type="text" id="hd_d" value="0" readonly><button type="button" onclick="tpChange('hd',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('hd',-1,'d')">−</button><input type="text" id="hd_d" value="0" readonly><button type="button" onclick="tpChange('hd',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'h')">−</button><input type="text" id="hd_h" value="00" readonly><button type="button" onclick="tpChange('hd',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'h')">−</button><input type="text" id="hd_h" value="00" readonly><button type="button" onclick="tpChange('hd',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'m')">−</button><input type="text" id="hd_m" value="30" readonly><button type="button" onclick="tpChange('hd',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('hd',-1,'m')">−</button><input type="text" id="hd_m" value="30" readonly><button type="button" onclick="tpChange('hd',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -711,8 +653,9 @@
|
|||||||
<script src="/js/card-defs.js"></script>
|
<script src="/js/card-defs.js"></script>
|
||||||
<script src="/js/card-display.js"></script>
|
<script src="/js/card-display.js"></script>
|
||||||
<script src="/js/icons.js"></script>
|
<script src="/js/icons.js"></script>
|
||||||
<script src="/js/nav.js"></script>
|
<script src="/js/nav.js"></script>
|
||||||
<script src="/js/social-sidebar.js"></script>
|
<script src="/js/social-sidebar.js"></script>
|
||||||
|
<script src="/js/time-picker.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||||
|
|
||||||
@@ -732,36 +675,6 @@
|
|||||||
let isLastPage = false;
|
let isLastPage = false;
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
|
|
||||||
// ── Zeitpicker ──
|
|
||||||
function tpChange(prefix, delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById(prefix + '_h').value) || 0;
|
|
||||||
let m = parseInt(document.getElementById(prefix + '_m').value) || 0;
|
|
||||||
if (seg === 'm') m += delta;
|
|
||||||
else if (seg === 'h') h += delta;
|
|
||||||
else d += delta;
|
|
||||||
if (m >= 60) { h += Math.floor(m/60); m %= 60; }
|
|
||||||
if (m < 0) { const b = Math.ceil(-m/60); h -= b; m += b*60; }
|
|
||||||
if (h >= 24) { d += Math.floor(h/24); h %= 24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById(prefix + '_d').value = d;
|
|
||||||
document.getElementById(prefix + '_h').value = String(h).padStart(2,'0');
|
|
||||||
document.getElementById(prefix + '_m').value = String(m).padStart(2,'0');
|
|
||||||
}
|
|
||||||
function tpToMinutes(prefix) {
|
|
||||||
const d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
|
||||||
const h = parseInt(document.getElementById(prefix + '_h').value) || 0;
|
|
||||||
const m = parseInt(document.getElementById(prefix + '_m').value) || 0;
|
|
||||||
return d*1440 + h*60 + m;
|
|
||||||
}
|
|
||||||
function tpFromMinutes(prefix, total) {
|
|
||||||
total = total || 0;
|
|
||||||
const d = Math.floor(total/1440), h = Math.floor((total%1440)/60), m = total%60;
|
|
||||||
document.getElementById(prefix + '_d').value = d;
|
|
||||||
document.getElementById(prefix + '_h').value = String(h).padStart(2,'0');
|
|
||||||
document.getElementById(prefix + '_m').value = String(m).padStart(2,'0');
|
|
||||||
}
|
|
||||||
function fmtMinutes(min) {
|
function fmtMinutes(min) {
|
||||||
if (!min) return '–';
|
if (!min) return '–';
|
||||||
const d = Math.floor(min/1440), h = Math.floor((min%1440)/60), m = min%60;
|
const d = Math.floor(min/1440), h = Math.floor((min%1440)/60), m = min%60;
|
||||||
@@ -850,8 +763,28 @@
|
|||||||
function checkGameCardSection() {
|
function checkGameCardSection() {
|
||||||
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
|
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
|
||||||
const maxV = parseInt(document.getElementById('max_GAME_CARD')?.value) || 0;
|
const maxV = parseInt(document.getElementById('max_GAME_CARD')?.value) || 0;
|
||||||
const sec = document.getElementById('sectionGameSetConfig');
|
const hasGame = minV > 0 || maxV > 0;
|
||||||
if (sec) sec.style.display = (minV > 0 || maxV > 0) ? '' : 'none';
|
const sub = document.getElementById('subGameSet');
|
||||||
|
if (sub) sub.style.display = hasGame ? '' : 'none';
|
||||||
|
checkAufgabenSetSection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkTaskCardSection() {
|
||||||
|
const minV = parseInt(document.getElementById('min_TASK')?.value) || 0;
|
||||||
|
const maxV = parseInt(document.getElementById('max_TASK')?.value) || 0;
|
||||||
|
const hasTask = minV > 0 || maxV > 0;
|
||||||
|
const sub = document.getElementById('subCardTaskSet');
|
||||||
|
if (sub) sub.style.display = hasTask ? '' : 'none';
|
||||||
|
checkAufgabenSetSection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAufgabenSetSection() {
|
||||||
|
const hasTask = (parseInt(document.getElementById('min_TASK')?.value) || 0) > 0
|
||||||
|
|| (parseInt(document.getElementById('max_TASK')?.value) || 0) > 0;
|
||||||
|
const hasGame = (parseInt(document.getElementById('min_GAME_CARD')?.value) || 0) > 0
|
||||||
|
|| (parseInt(document.getElementById('max_GAME_CARD')?.value) || 0) > 0;
|
||||||
|
const sec = document.getElementById('sectionAufgabenSet');
|
||||||
|
if (sec) sec.style.display = (hasTask || hasGame) ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGameSpieldauer(val) {
|
function updateGameSpieldauer(val) {
|
||||||
@@ -980,10 +913,8 @@
|
|||||||
<div id="we-tp-${id}" style="display:none;">
|
<div id="we-tp-${id}" style="display:none;">
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'d')">−</button><input type="text" id="wt${id}_d" value="0" readonly><button type="button" onclick="tpChange('wt${id}',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('wt${id}',-1,'d')">−</button><input type="text" id="wt${id}_d" value="0" readonly><button type="button" onclick="tpChange('wt${id}',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'h')">−</button><input type="text" id="wt${id}_h" value="01" readonly><button type="button" onclick="tpChange('wt${id}',1,'h')">+</button></div><span class="tp-label">Stunden</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'h')">−</button><input type="text" id="wt${id}_h" value="01" readonly><button type="button" onclick="tpChange('wt${id}',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'m')">−</button><input type="text" id="wt${id}_m" value="00" readonly><button type="button" onclick="tpChange('wt${id}',1,'m')">+</button></div><span class="tp-label">Minuten</span></div>
|
||||||
<div class="tp-colon">:</div>
|
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'m')">−</button><input type="text" id="wt${id}_m" value="00" readonly><button type="button" onclick="tpChange('wt${id}',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="we-str-${id}" placeholder="Text…" maxlength="200"
|
<input type="text" id="we-str-${id}" placeholder="Text…" maxlength="200"
|
||||||
@@ -1328,7 +1259,7 @@
|
|||||||
function alignModalToContent() {
|
function alignModalToContent() {
|
||||||
const rect = document.querySelector('.content')?.getBoundingClientRect();
|
const rect = document.querySelector('.content')?.getBoundingClientRect();
|
||||||
if (!rect) return;
|
if (!rect) return;
|
||||||
document.getElementById('modalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 720) + 'px';
|
document.getElementById('modalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 800) + 'px';
|
||||||
document.getElementById('taskSetModalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 900) + 'px';
|
document.getElementById('taskSetModalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 900) + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1367,7 +1298,8 @@
|
|||||||
document.getElementById('fAccumulate').checked = template?.accumulatePicks || false;
|
document.getElementById('fAccumulate').checked = template?.accumulatePicks || false;
|
||||||
document.getElementById('fShowRemaining').checked = template?.showRemainingCards || false;
|
document.getElementById('fShowRemaining').checked = template?.showRemainingCards || false;
|
||||||
|
|
||||||
// Spiel-Karte
|
// Task-Karte und Spiel-Karte
|
||||||
|
checkTaskCardSection();
|
||||||
clearGameSet();
|
clearGameSet();
|
||||||
checkGameCardSection();
|
checkGameCardSection();
|
||||||
const gsi = template?.gameSpieldauerIdx ?? 2;
|
const gsi = template?.gameSpieldauerIdx ?? 2;
|
||||||
@@ -1528,7 +1460,7 @@
|
|||||||
const totalMax = CARD_DEFS.reduce((s,c)=>s+(parseInt(document.getElementById('max_'+c.id).value)||0),0);
|
const totalMax = CARD_DEFS.reduce((s,c)=>s+(parseInt(document.getElementById('max_'+c.id).value)||0),0);
|
||||||
if (totalMax===0) { showModalError('Das Deck muss mindestens eine Karte enthalten.'); firstError=firstError||document.getElementById('modalError'); }
|
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;
|
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'); }
|
if (hasTaskCards && !document.getElementById('fCardTaskSetId').value) { showModalError('Bitte ein Aufgaben-Set für die Aufgaben-Karten auswählen.'); 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;
|
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) {
|
if (hasGameCards && !document.getElementById('fGameSetId').value) {
|
||||||
document.getElementById('errGameSet').style.display = '';
|
document.getElementById('errGameSet').style.display = '';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>Neues Lock – xXx Sphere</title>
|
<title>Neues Lock – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link rel="stylesheet" href="/css/time-picker.css">
|
||||||
<style>
|
<style>
|
||||||
.form-section {
|
.form-section {
|
||||||
background: var(--color-card);
|
background: var(--color-card);
|
||||||
@@ -108,26 +109,6 @@
|
|||||||
.field-error input { border-color: #e74c3c !important; }
|
.field-error input { border-color: #e74c3c !important; }
|
||||||
.field-error-msg { font-size: 0.78rem; color: #e74c3c; margin-top: 0.15rem; }
|
.field-error-msg { font-size: 0.78rem; color: #e74c3c; margin-top: 0.15rem; }
|
||||||
|
|
||||||
/* Zeitpicker */
|
|
||||||
.time-picker { display:flex; align-items:center; gap:0.4rem; flex-wrap:wrap; }
|
|
||||||
.tp-seg { display:flex; flex-direction:column; align-items:center; gap:0.15rem; }
|
|
||||||
.tp-seg-row { display:flex; align-items:center; gap:0.2rem; }
|
|
||||||
.tp-seg button {
|
|
||||||
width:24px; height:24px; background:var(--color-card);
|
|
||||||
border:1px solid var(--color-muted); border-radius:4px;
|
|
||||||
cursor:pointer; font-size:0.9rem; font-weight:700; color:var(--color-text);
|
|
||||||
display:flex; align-items:center; justify-content:center; padding:0; flex-shrink:0;
|
|
||||||
}
|
|
||||||
.tp-seg button:hover { background:var(--color-primary); color:#fff; border-color:var(--color-primary); }
|
|
||||||
.tp-seg input {
|
|
||||||
width:28px; text-align:center; background:var(--color-card);
|
|
||||||
border:1px solid var(--color-muted); border-radius:4px;
|
|
||||||
color:var(--color-text); font-size:0.9rem; font-weight:600;
|
|
||||||
font-family:monospace; padding:0.15rem 0; box-sizing:border-box;
|
|
||||||
}
|
|
||||||
.tp-seg .tp-label { font-size:0.62rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; }
|
|
||||||
.tp-colon { font-size:1rem; font-weight:700; color:var(--color-muted); margin-bottom:0.9rem; }
|
|
||||||
|
|
||||||
/* LockControl-Auswahl */
|
/* LockControl-Auswahl */
|
||||||
.lockcontrol-options { display: flex; flex-direction: column; gap: 0.6rem; }
|
.lockcontrol-options { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||||
.lockcontrol-option {
|
.lockcontrol-option {
|
||||||
@@ -225,23 +206,8 @@
|
|||||||
<div class="form-row" id="rowMaxDuration">
|
<div class="form-row" id="rowMaxDuration">
|
||||||
<label>Längste Dauer</label>
|
<label>Längste Dauer</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg">
|
<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-row">
|
<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>
|
||||||
<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-colon">:</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">Std</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-hint">Das Lock öffnet spätestens nach dieser Zeit automatisch. 0 : 00 = keine Begrenzung.</div>
|
<div class="form-hint">Das Lock öffnet spätestens nach dieser Zeit automatisch. 0 : 00 = keine Begrenzung.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -359,8 +325,9 @@
|
|||||||
<script src="/js/card-defs.js"></script>
|
<script src="/js/card-defs.js"></script>
|
||||||
<script src="/js/shared.js"></script>
|
<script src="/js/shared.js"></script>
|
||||||
<script src="/js/icons.js"></script>
|
<script src="/js/icons.js"></script>
|
||||||
<script src="/js/nav.js"></script>
|
<script src="/js/nav.js"></script>
|
||||||
<script src="/js/social-sidebar.js"></script>
|
<script src="/js/social-sidebar.js"></script>
|
||||||
|
<script src="/js/time-picker.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let myUserId = null;
|
let myUserId = null;
|
||||||
let myUserName = null;
|
let myUserName = null;
|
||||||
@@ -655,20 +622,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Zeitpicker ──
|
|
||||||
function tpChange(prefix, delta, seg) {
|
|
||||||
let d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
|
||||||
let h = parseInt(document.getElementById(prefix + '_h')?.value) || 0;
|
|
||||||
if (seg === 'h') h += delta;
|
|
||||||
else d += delta;
|
|
||||||
if (h >= 24) { d += Math.floor(h / 24); h %= 24; }
|
|
||||||
if (h < 0) { const b = Math.ceil(-h / 24); d -= b; h += b * 24; }
|
|
||||||
if (d < 0) d = 0;
|
|
||||||
document.getElementById(prefix + '_d').value = d;
|
|
||||||
if (document.getElementById(prefix + '_h'))
|
|
||||||
document.getElementById(prefix + '_h').value = String(h).padStart(2, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Längste Dauer → LocalDateTime ──
|
// ── Längste Dauer → LocalDateTime ──
|
||||||
function durationToLatestOpening() {
|
function durationToLatestOpening() {
|
||||||
const days = parseInt(document.getElementById('dur_d').value) || 0;
|
const days = parseInt(document.getElementById('dur_d').value) || 0;
|
||||||
@@ -843,6 +796,8 @@
|
|||||||
requiresVerification: t.requiresVerification,
|
requiresVerification: t.requiresVerification,
|
||||||
testLock: isTestLock,
|
testLock: isTestLock,
|
||||||
controllType: selectedLockControl,
|
controllType: selectedLockControl,
|
||||||
|
gameSetId: t.gameSetId || null,
|
||||||
|
gameSpieldauerIdx: t.gameSpieldauerIdx ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@
|
|||||||
<h2>Level 6 erreicht!</h2>
|
<h2>Level 6 erreicht!</h2>
|
||||||
<div class="game-label" id="finisherTitle"></div>
|
<div class="game-label" id="finisherTitle"></div>
|
||||||
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
|
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
|
||||||
<button class="btn-secondary" onclick="goBack()" style="margin-top:1.25rem;">Zurück zum Lock</button>
|
<button class="btn-primary" onclick="completeGame()" style="margin-top:1.25rem;">✓ Spiel beenden</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -189,8 +189,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const lockId = params.get('lockId');
|
const lockId = params.get('lockId');
|
||||||
|
const autoGameSetId = params.get('gameSetId');
|
||||||
let _state = null;
|
let _state = null;
|
||||||
let _timerInt = null;
|
let _timerInt = null;
|
||||||
let _pendingIsLock = false;
|
let _pendingIsLock = false;
|
||||||
@@ -201,13 +202,23 @@
|
|||||||
else history.back();
|
else history.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function completeGame() {
|
||||||
|
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
|
||||||
|
await fetch(url, { method: 'POST' });
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Init ──────────────────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/lock-game/state');
|
const r = await fetch('/lock-game/state');
|
||||||
if (r.status === 404) {
|
if (r.status === 404) {
|
||||||
await loadGroups();
|
if (autoGameSetId) {
|
||||||
|
await autoStartGame(autoGameSetId);
|
||||||
|
} else {
|
||||||
|
await loadGroups();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
|
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
|
||||||
@@ -219,6 +230,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function autoStartGame(gameSetId) {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + gameSetId, { method: 'POST' });
|
||||||
|
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||||
|
const stateR = await fetch('/lock-game/state');
|
||||||
|
_state = await stateR.json();
|
||||||
|
hide('loadingHint');
|
||||||
|
await runGameLoop();
|
||||||
|
} catch (e) {
|
||||||
|
showError(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadGroups() {
|
async function loadGroups() {
|
||||||
const r = await fetch('/lock-game/groups');
|
const r = await fetch('/lock-game/groups');
|
||||||
const groups = r.ok ? await r.json() : [];
|
const groups = r.ok ? await r.json() : [];
|
||||||
|
|||||||
BIN
src/main/resources/static/img/icons/bell.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/main/resources/static/img/icons/envelope.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
src/main/resources/static/img/icons/lock.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
src/main/resources/static/img/icons/stars.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
54
src/main/resources/static/js/time-picker.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
function tpChange(prefix, delta, seg) {
|
||||||
|
let d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
||||||
|
let h = parseInt(document.getElementById(prefix + '_h')?.value) || 0;
|
||||||
|
let m = parseInt(document.getElementById(prefix + '_m')?.value) || 0;
|
||||||
|
if (seg === 'm') m += delta;
|
||||||
|
else if (seg === 'h') h += delta;
|
||||||
|
else d += delta;
|
||||||
|
if (m >= 60) { h += Math.floor(m/60); m %= 60; }
|
||||||
|
if (m < 0) { const b = Math.ceil(-m/60); h -= b; m += b*60; }
|
||||||
|
if (h >= 24) { d += Math.floor(h/24); h %= 24; }
|
||||||
|
if (h < 0) { const b = Math.ceil(-h/24); d -= b; h += b*24; }
|
||||||
|
if (d < 0) d = 0;
|
||||||
|
document.getElementById(prefix + '_d').value = d;
|
||||||
|
if (document.getElementById(prefix + '_h'))
|
||||||
|
document.getElementById(prefix + '_h').value = String(h).padStart(2, '0');
|
||||||
|
if (document.getElementById(prefix + '_m'))
|
||||||
|
document.getElementById(prefix + '_m').value = String(m).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tpToMinutes(prefix) {
|
||||||
|
const d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
||||||
|
const h = parseInt(document.getElementById(prefix + '_h')?.value) || 0;
|
||||||
|
const m = parseInt(document.getElementById(prefix + '_m')?.value) || 0;
|
||||||
|
return d * 1440 + h * 60 + m;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tpFromMinutes(prefix, total) {
|
||||||
|
total = total || 0;
|
||||||
|
const d = Math.floor(total / 1440);
|
||||||
|
const h = Math.floor((total % 1440) / 60);
|
||||||
|
const m = total % 60;
|
||||||
|
document.getElementById(prefix + '_d').value = d;
|
||||||
|
if (document.getElementById(prefix + '_h'))
|
||||||
|
document.getElementById(prefix + '_h').value = String(h).padStart(2, '0');
|
||||||
|
if (document.getElementById(prefix + '_m'))
|
||||||
|
document.getElementById(prefix + '_m').value = String(m).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tpHtml(prefix, initD, initH, initM) {
|
||||||
|
const d = initD ?? 0, h = initH ?? 0, m = initM ?? 0;
|
||||||
|
return `<div class="time-picker">` +
|
||||||
|
`<div class="tp-seg"><button type="button" onclick="tpChange('${prefix}',-1,'d')">−</button><input type="text" id="${prefix}_d" value="${d}" readonly><button type="button" onclick="tpChange('${prefix}',1,'d')">+</button><span class="tp-label">Tage</span></div>` +
|
||||||
|
`<div class="tp-seg"><button type="button" onclick="tpChange('${prefix}',-1,'h')">−</button><input type="text" id="${prefix}_h" value="${String(h).padStart(2,'0')}" readonly><button type="button" onclick="tpChange('${prefix}',1,'h')">+</button><span class="tp-label">Stunden</span></div>` +
|
||||||
|
`<div class="tp-seg"><button type="button" onclick="tpChange('${prefix}',-1,'m')">−</button><input type="text" id="${prefix}_m" value="${String(m).padStart(2,'0')}" readonly><button type="button" onclick="tpChange('${prefix}',1,'m')">+</button><span class="tp-label">Minuten</span></div>` +
|
||||||
|
`</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tpHtmlDH(prefix, initD, initH) {
|
||||||
|
const d = initD ?? 0, h = initH ?? 0;
|
||||||
|
return `<div class="time-picker">` +
|
||||||
|
`<div class="tp-seg"><button type="button" onclick="tpChange('${prefix}',-1,'d')">−</button><input type="text" id="${prefix}_d" value="${d}" readonly><button type="button" onclick="tpChange('${prefix}',1,'d')">+</button><span class="tp-label">Tage</span></div>` +
|
||||||
|
`<div class="tp-seg"><button type="button" onclick="tpChange('${prefix}',-1,'h')">−</button><input type="text" id="${prefix}_h" value="${String(h).padStart(2,'0')}" readonly><button type="button" onclick="tpChange('${prefix}',1,'h')">+</button><span class="tp-label">Stunden</span></div>` +
|
||||||
|
`</div>`;
|
||||||
|
}
|
||||||
@@ -35,15 +35,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="topbar-right">
|
<div class="topbar-right">
|
||||||
<button class="topbar-btn" id="topbarMsgBtn" title="Nachrichten">
|
<button class="topbar-btn" id="topbarMsgBtn" title="Nachrichten">
|
||||||
${IC('MESSAGES')}
|
<img src="/img/icons/envelope.png" style="width:1.875rem;height:1.875rem;object-fit:contain;display:block;" alt="">
|
||||||
<span class="topbar-badge" id="topbarMsgBadge"></span>
|
<span class="topbar-badge" id="topbarMsgBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="topbar-btn" id="topbarNotifBtn" title="Benachrichtigungen">
|
<button class="topbar-btn" id="topbarNotifBtn" title="Benachrichtigungen">
|
||||||
${IC('NOTIFICATIONS')}
|
<img src="/img/icons/bell.png" style="width:1.875rem;height:1.875rem;object-fit:contain;display:block;" alt="">
|
||||||
<span class="topbar-badge" id="topbarNotifBadge"></span>
|
<span class="topbar-badge" id="topbarNotifBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="topbar-btn" id="topbarInvBtn" title="Einladungen">
|
<button class="topbar-btn" id="topbarInvBtn" title="Einladungen">
|
||||||
${IC('INVITATIONS')}
|
<img src="/img/icons/stars.png" style="width:1.875rem;height:1.875rem;object-fit:contain;display:block;" alt="">
|
||||||
<span class="topbar-badge" id="topbarInvBadge"></span>
|
<span class="topbar-badge" id="topbarInvBadge"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="topbar-btn topbar-profile-btn" id="topbarProfileBtn">
|
<button class="topbar-btn topbar-profile-btn" id="topbarProfileBtn">
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
document.body.insertAdjacentHTML('beforeend', `
|
document.body.insertAdjacentHTML('beforeend', `
|
||||||
<div class="topbar-panel" id="topbarMsgPanel">
|
<div class="topbar-panel" id="topbarMsgPanel">
|
||||||
<div class="topbar-panel-header">
|
<div class="topbar-panel-header">
|
||||||
<span>${IC('MESSAGES')} Nachrichten</span>
|
<span><img src="/img/icons/envelope.png" style="width:1rem;height:1rem;object-fit:contain;vertical-align:middle;margin-right:0.3rem;" alt=""> Nachrichten</span>
|
||||||
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-panel-body" id="topbarMsgBody"></div>
|
<div class="topbar-panel-body" id="topbarMsgBody"></div>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
<div class="topbar-panel" id="topbarNotifPanel">
|
<div class="topbar-panel" id="topbarNotifPanel">
|
||||||
<div class="topbar-panel-header">
|
<div class="topbar-panel-header">
|
||||||
<span>${IC('NOTIFICATIONS')} Benachrichtigungen</span>
|
<span><img src="/img/icons/bell.png" style="width:1rem;height:1rem;object-fit:contain;vertical-align:middle;margin-right:0.3rem;" alt=""> Benachrichtigungen</span>
|
||||||
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-panel-body" id="topbarNotifBody"></div>
|
<div class="topbar-panel-body" id="topbarNotifBody"></div>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
<div class="topbar-panel" id="topbarInvPanel">
|
<div class="topbar-panel" id="topbarInvPanel">
|
||||||
<div class="topbar-panel-header">
|
<div class="topbar-panel-header">
|
||||||
<span>${IC('INVITATIONS')} Einladungen</span>
|
<span><img src="/img/icons/stars.png" style="width:1rem;height:1rem;object-fit:contain;vertical-align:middle;margin-right:0.3rem;" alt=""> Einladungen</span>
|
||||||
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-panel-body" id="topbarInvBody"></div>
|
<div class="topbar-panel-body" id="topbarInvBody"></div>
|
||||||
|
|||||||