Bugfixes im Vanilla game und refactoring
@@ -1,5 +1,5 @@
|
||||
#Sun Mar 29 16:28:09 CEST 2026
|
||||
#Mon Mar 30 07:33:01 CEST 2026
|
||||
display=\:0
|
||||
host=mario-mint
|
||||
process-id=3723
|
||||
process-id=2955
|
||||
user=mario
|
||||
|
||||
@@ -1335,3 +1335,47 @@ java.io.IOException: Stream closed
|
||||
|
||||
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-29 22:58:21.587
|
||||
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
|
||||
!SESSION 2026-03-30 07:32:31.775 -----------------------------------------------
|
||||
eclipse.buildId=4.39.0.20260305-0817
|
||||
java.version=21.0.6
|
||||
java.vendor=Eclipse Adoptium
|
||||
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
|
||||
Framework arguments: -product org.eclipse.epp.package.java.product
|
||||
Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product
|
||||
|
||||
!ENTRY ch.qos.logback.classic 1 0 2026-03-30 07:32:34.763
|
||||
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
|
||||
|
||||
!ENTRY ch.qos.logback.classic 1 0 2026-03-30 07:33:02.086
|
||||
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
|
||||
|
||||
!ENTRY org.eclipse.ui 2 0 2026-03-30 07:33:02.235
|
||||
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-30 07:33:02.235
|
||||
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||
|
||||
!ENTRY org.eclipse.ui 2 0 2026-03-30 07:33:02.395
|
||||
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-30 07:33:02.395
|
||||
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||
|
||||
!ENTRY org.eclipse.jface 2 0 2026-03-30 22:51:45.208
|
||||
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-30 22:51:45.208
|
||||
!MESSAGE A conflict occurred for CTRL+SHIFT+T:
|
||||
Binding(CTRL+SHIFT+T,
|
||||
ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type,
|
||||
Open a type in a Java editor,
|
||||
Category(org.eclipse.ui.category.navigate,Navigate,null,true),
|
||||
WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"),
|
||||
,,true),null),
|
||||
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||
org.eclipse.ui.contexts.window,,,system)
|
||||
Binding(CTRL+SHIFT+T,
|
||||
ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace,
|
||||
,
|
||||
Category(org.eclipse.lsp4e.category,Language Servers,null,true),
|
||||
WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"),
|
||||
,,true),null),
|
||||
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||
org.eclipse.ui.contexts.window,,,system)
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
2026-03-26 16:50:11,098 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
||||
2026-03-27 07:46:24,300 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
||||
2026-03-29 16:28:13,219 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
|
||||
2026-03-30 07:33:05,316 [Worker-5: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#Sun Mar 29 16:28:09 CEST 2026
|
||||
#Mon Mar 30 07:33:01 CEST 2026
|
||||
org.eclipse.core.runtime=2
|
||||
org.eclipse.platform=4.39.0.v20260226-0420
|
||||
|
||||
BIN
bilder/toys/bodenpranger.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
bilder/toys/prangerhandfuss.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
bilder/toys/prangerhandhals.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 14 KiB |
BIN
bilder/toys/raw/41FbgSjf5kL._AC_SL1000_.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
bilder/toys/raw/il_1140xN.6566343643_8mh3.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
bilder/toys/raw/il_794xN.6730546998_e67q.webp
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
@@ -254,6 +254,40 @@ public class AdminController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Item verschieben ─────────────────────────────────────────────────────
|
||||
|
||||
@PutMapping("/aufgabengruppen/items/{kind}/{itemId}/move")
|
||||
public ResponseEntity<Void> moveItem(
|
||||
@PathVariable("kind") String kind,
|
||||
@PathVariable("itemId") UUID itemId,
|
||||
@RequestParam("targetGruppeId") UUID targetGruppeId,
|
||||
Principal principal) {
|
||||
requireAdmin(principal);
|
||||
AufgabenGruppeEntity targetGruppe = aufgabenGruppeRepository.findById(targetGruppeId)
|
||||
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.NOT_FOUND, "Zielgruppe nicht gefunden"));
|
||||
switch (kind) {
|
||||
case "aufgabe" -> aufgabeRepository.findById(itemId).ifPresent(e -> {
|
||||
e.setAufgabenGruppe(targetGruppe);
|
||||
aufgabeRepository.save(e);
|
||||
});
|
||||
case "strafe" -> strafeRepository.findById(itemId).ifPresent(e -> {
|
||||
e.setAufgabenGruppe(targetGruppe);
|
||||
strafeRepository.save(e);
|
||||
});
|
||||
case "zeitstrafe" -> sperreRepository.findById(itemId).ifPresent(e -> {
|
||||
e.setAufgabenGruppe(targetGruppe);
|
||||
sperreRepository.save(e);
|
||||
});
|
||||
case "finisher" -> finisherRepository.findById(itemId).ifPresent(e -> {
|
||||
e.setAufgabenGruppe(targetGruppe);
|
||||
finisherRepository.save(e);
|
||||
});
|
||||
default -> { return ResponseEntity.badRequest().build(); }
|
||||
}
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Toys ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/toys")
|
||||
|
||||
@@ -49,12 +49,10 @@ public class SecurityConfig {
|
||||
.requestMatchers("/games/chastity/sessionchastity.html").authenticated()
|
||||
.requestMatchers("/games/chastity/neulock.html").authenticated()
|
||||
.requestMatchers("/games/chastity/activelock.html").authenticated()
|
||||
.requestMatchers("/sessionbdsmtasks.html").authenticated()
|
||||
.requestMatchers("/sessionbdsmtoys.html").authenticated()
|
||||
.requestMatchers("/sessionbdsmingame.html").authenticated()
|
||||
.requestMatchers("/games/bdsm/neubdsm.html").authenticated()
|
||||
.requestMatchers("/games/bdsm/bdsmingame.html").authenticated()
|
||||
.requestMatchers("/games/bdsm/bdsmwarten.html").authenticated()
|
||||
.requestMatchers("/community/personen-suchen.html").authenticated()
|
||||
.requestMatchers("/community/freunde.html").authenticated()
|
||||
.requestMatchers("/community/nachrichten.html").authenticated()
|
||||
@@ -69,7 +67,7 @@ public class SecurityConfig {
|
||||
.requestMatchers("/games/chastity/meine-locks.html").authenticated()
|
||||
.requestMatchers("/games/chastity/entdecken-vorlagen.html").authenticated()
|
||||
.requestMatchers("/games/chastity/unlock-history.html").authenticated()
|
||||
.requestMatchers("/community/einladungen.html").authenticated()
|
||||
.requestMatchers("/games/common/einladungen.html").authenticated()
|
||||
.requestMatchers("/games/chastity/joinlock.html").authenticated()
|
||||
.requestMatchers("/community/benachrichtigungen.html").authenticated()
|
||||
.requestMatchers("/community/abonnements.html").authenticated()
|
||||
|
||||
@@ -23,7 +23,6 @@ import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity.Status;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.social.repository.FriendshipRepository;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
@@ -97,13 +96,7 @@ public class BdsmEinladungController {
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
einladungRepository.save(entity);
|
||||
|
||||
String inviterName = userRepository.findById(inviterId).map(u -> u.getName()).orElse("Jemand");
|
||||
systemMessageService.send(
|
||||
inviterId, req.inviteeId(),
|
||||
inviterName + " hat dich zum BDSM Game eingeladen.",
|
||||
"/community/einladungen.html",
|
||||
MessageCause.INVITATION
|
||||
);
|
||||
systemMessageService.pushInvitationUpdate(req.inviteeId());
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("einladungId", entity.getEinladungId());
|
||||
@@ -118,10 +111,7 @@ public class BdsmEinladungController {
|
||||
if (e == null) return ResponseEntity.notFound().build();
|
||||
if (!e.getInviterId().equals(userId)) return ResponseEntity.status(403).build();
|
||||
e.setStatus(Status.CANCELLED);
|
||||
String inviterName = userRepository.findById(userId).map(u -> u.getName()).orElse("Jemand");
|
||||
systemMessageService.send(userId, e.getInviteeId(),
|
||||
inviterName + " hat die BDSM-Spieleinladung zurückgezogen.",
|
||||
"/community/einladungen.html", MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(e.getInviteeId());
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -189,10 +189,7 @@ public class CardLockController {
|
||||
inv.setDetailsVisible(req.lockeeDetailsVisible());
|
||||
lockeeInvitationRepository.save(inv);
|
||||
|
||||
String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock";
|
||||
sendMessage(myId, lockee.getUserId(),
|
||||
me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.",
|
||||
"/community/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(lockee.getUserId());
|
||||
|
||||
return ResponseEntity.ok(Map.of("lockId", lock.getLockId().toString(), "lockeeInvitationSent", true));
|
||||
}
|
||||
@@ -261,10 +258,7 @@ public class CardLockController {
|
||||
inv.setCreatedAt(now);
|
||||
invitationRepository.save(inv);
|
||||
|
||||
String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock";
|
||||
sendMessage(me.getUserId(), kh.getUserId(),
|
||||
me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.",
|
||||
"/community/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(kh.getUserId());
|
||||
|
||||
keyholderPending = true;
|
||||
}
|
||||
@@ -772,10 +766,7 @@ public class CardLockController {
|
||||
|
||||
if (lockOpt.isPresent()) {
|
||||
var lock = lockOpt.get();
|
||||
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
|
||||
sendMessage(myId, lock.getLockee(),
|
||||
me.getName() + " hat die Einladung als Keyholder*In für das Lock „" + lockName + "\" abgelehnt.",
|
||||
null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(lock.getLockee());
|
||||
}
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
@@ -828,12 +819,7 @@ public class CardLockController {
|
||||
|
||||
invitationRepository.delete(inv);
|
||||
|
||||
String lockName = lockOpt.get().getName() != null && !lockOpt.get().getName().isBlank()
|
||||
? lockOpt.get().getName()
|
||||
: "Unbenanntes Lock";
|
||||
sendMessage(myId, inv.getKeyholderUserId(),
|
||||
me.getName() + " hat die Keyholder-Einladung für das Lock „" + lockName + "\" zurückgezogen.", null,
|
||||
de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(inv.getKeyholderUserId());
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import de.oaa.xxx.games.chastity.timelock.TimeLockRepository;
|
||||
import de.oaa.xxx.games.chastity.timelock.TimeLockServiceFactory;
|
||||
import de.oaa.xxx.games.chastity.timelock.TimeLockTemplateEntity;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
@@ -319,7 +318,6 @@ public class KeyholderOfferController {
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
|
||||
String lockName = template.getName() != null ? template.getName() : "Unbenanntes Lock";
|
||||
boolean invitationSent = false;
|
||||
if (!directStart) {
|
||||
// Normaler Einladungsworkflow: Keyholder muss bestätigen
|
||||
@@ -331,17 +329,11 @@ public class KeyholderOfferController {
|
||||
inv.setCreatedAt(LocalDateTime.now());
|
||||
invitationRepository.save(inv);
|
||||
|
||||
systemMessageService.send(myId, offerer.getUserId(),
|
||||
me.getName() + " möchte dein Keyholder-Angebot annehmen und lädt dich als Keyholder für „"
|
||||
+ lockName + "\" ein.",
|
||||
"/community/einladungen.html", MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(offerer.getUserId());
|
||||
invitationSent = true;
|
||||
} else {
|
||||
// Direktstart: Keyholder wird direkt gesetzt, aber trotzdem benachrichtigen
|
||||
systemMessageService.send(myId, offerer.getUserId(),
|
||||
me.getName() + " hat dein Keyholder-Angebot angenommen und das Lock „"
|
||||
+ lockName + "\" gestartet.",
|
||||
"/games/chastity/keyholder.html", MessageCause.INVITATION);
|
||||
// Direktstart: Keyholder wird direkt gesetzt
|
||||
systemMessageService.pushInvitationUpdate(offerer.getUserId());
|
||||
}
|
||||
|
||||
// Annahmezähler erhöhen
|
||||
|
||||
@@ -65,10 +65,6 @@ public class LockeeInvitationController {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl, de.oaa.xxx.social.entity.MessageCause cause) {
|
||||
systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
|
||||
}
|
||||
|
||||
private String generateUnlockCode(int lines) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < lines; i++) sb.append(RNG.nextInt(10));
|
||||
@@ -165,10 +161,7 @@ public class LockeeInvitationController {
|
||||
if (lockOpt.isPresent()) {
|
||||
var lock = lockOpt.get();
|
||||
baseLockRepository.delete(lock);
|
||||
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
|
||||
sendMessage(myId, inv.getLockeeUserId(),
|
||||
me.getName() + " hat die Lockee-Einladung für das Lock „" + lockName + "\" zurückgezogen.",
|
||||
null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(inv.getLockeeUserId());
|
||||
}
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
@@ -245,7 +238,6 @@ public class LockeeInvitationController {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
String unlockCode;
|
||||
String lockName;
|
||||
|
||||
if (lock instanceof CardLockEntity cardLock) {
|
||||
unlockCode = generateUnlockCode(codeLines);
|
||||
@@ -259,7 +251,6 @@ public class LockeeInvitationController {
|
||||
cardLock.setLastHygineOpening(now);
|
||||
}
|
||||
cardlockRepository.save(cardLock);
|
||||
lockName = cardLock.getName() != null && !cardLock.getName().isBlank() ? cardLock.getName() : "Unbenanntes Lock";
|
||||
} else if (lock instanceof TimeLockEntity timeLock) {
|
||||
unlockCode = CodeCreator.createNumeric(codeLines);
|
||||
int unlockMinutes = randomBetween(timeLock.getMinTimeInMinutes(), timeLock.getMaxTimeInMinutes());
|
||||
@@ -271,16 +262,13 @@ public class LockeeInvitationController {
|
||||
timeLock.setLastHygineOpening(now);
|
||||
}
|
||||
timeLockRepository.save(timeLock);
|
||||
lockName = timeLock.getName() != null && !timeLock.getName().isBlank() ? timeLock.getName() : "Unbenanntes Lock";
|
||||
} else {
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
|
||||
lockeeInvitationRepository.delete(inv);
|
||||
|
||||
sendMessage(myId, inv.getKeyholderUserId(),
|
||||
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" angenommen.",
|
||||
"/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(inv.getKeyholderUserId());
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"lockId", lock.getLockId().toString(),
|
||||
@@ -305,10 +293,7 @@ public class LockeeInvitationController {
|
||||
if (lockOpt.isPresent()) {
|
||||
var lock = lockOpt.get();
|
||||
baseLockRepository.delete(lock);
|
||||
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
|
||||
sendMessage(myId, inv.getKeyholderUserId(),
|
||||
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" abgelehnt.",
|
||||
null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(inv.getKeyholderUserId());
|
||||
}
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
|
||||
@@ -140,10 +140,7 @@ public class TimeLockController {
|
||||
inv.setDetailsVisible(req.lockeeDetailsVisible());
|
||||
lockeeInvitationRepository.save(inv);
|
||||
|
||||
String lockName = template.getName() != null ? template.getName() : "Unbenanntes Lock";
|
||||
systemMessageService.send(myId, lockee.getUserId(),
|
||||
me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.",
|
||||
"/community/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(lockee.getUserId());
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"lockId", lock.getLockId().toString(),
|
||||
@@ -178,10 +175,7 @@ public class TimeLockController {
|
||||
inv.setCreatedAt(LocalDateTime.now());
|
||||
invitationRepository.save(inv);
|
||||
|
||||
String lockName = template.getName() != null ? template.getName() : "Unbenanntes Lock";
|
||||
systemMessageService.send(myId, kh.getUserId(),
|
||||
me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.",
|
||||
"/community/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(kh.getUserId());
|
||||
|
||||
keyholderPending = true;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.oaa.xxx.games.common.repository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
@@ -37,4 +38,10 @@ public interface AufgabenGruppeRepository extends JpaRepository<AufgabenGruppeEn
|
||||
|
||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.privateGruppe = false AND g.userId IS NOT NULL AND g.userId <> :userId AND g.strafen IS EMPTY AND g.sperren IS EMPTY AND (:name IS NULL OR LOWER(g.name) LIKE LOWER(:name))")
|
||||
List<AufgabenGruppeEntity> findVanillaSafePublicFromOthers(@Param("userId") UUID userId, @Param("name") String name);
|
||||
|
||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.userId = :userId AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)")
|
||||
Page<AufgabenGruppeEntity> findByUserIdWithContent(@Param("userId") UUID userId, Pageable pageable);
|
||||
|
||||
@Query("SELECT g FROM AufgabenGruppeEntity g WHERE g.userId IS NULL AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)")
|
||||
Page<AufgabenGruppeEntity> findSystemGroupsWithContent(Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package de.oaa.xxx.games.common.repository;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.games.common.entity.GruppenAboEntity;
|
||||
@@ -19,4 +23,8 @@ public interface GruppenAboRepository extends JpaRepository<GruppenAboEntity, UU
|
||||
long countByAufgabenGruppe(AufgabenGruppeEntity gruppe);
|
||||
|
||||
void deleteByAufgabenGruppe(AufgabenGruppeEntity gruppe);
|
||||
|
||||
@Query(value = "SELECT a FROM GruppenAboEntity a JOIN a.aufgabenGruppe g WHERE a.userId = :userId AND g.privateGruppe = false AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)",
|
||||
countQuery = "SELECT COUNT(a) FROM GruppenAboEntity a JOIN a.aufgabenGruppe g WHERE a.userId = :userId AND g.privateGruppe = false AND (g.aufgaben IS NOT EMPTY OR g.finisher IS NOT EMPTY)")
|
||||
Page<GruppenAboEntity> findByUserIdWithContent(@Param("userId") UUID userId, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package de.oaa.xxx.games.vanilla.controller;
|
||||
|
||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppe;
|
||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppePage;
|
||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.games.common.entity.GruppenAboEntity;
|
||||
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.games.common.repository.GruppenAboRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
@@ -18,13 +19,14 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppe;
|
||||
import de.oaa.xxx.games.common.aufgaben.AufgabenGruppePage;
|
||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.games.common.entity.GruppenAboEntity;
|
||||
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.games.common.repository.GruppenAboRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/vanilla/abo")
|
||||
@@ -56,15 +58,17 @@ public class VanillaAboController {
|
||||
Principal principal) {
|
||||
UserEntity user = userService.requireUser(principal);
|
||||
|
||||
List<AufgabenGruppe> dtos = aboRepository.findByUserId(user.getUserId()).stream()
|
||||
.map(GruppenAboEntity::getAufgabenGruppe)
|
||||
.filter(g -> !g.isPrivateGruppe())
|
||||
.filter(g -> g.getStrafen().isEmpty() && g.getSperren().isEmpty())
|
||||
.map(g -> enrich(g, user.getUserId(), true))
|
||||
.sorted(Comparator.comparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
||||
Page<GruppenAboEntity> dbPage = aboRepository.findByUserIdWithContent(
|
||||
user.getUserId(), PageRequest.of(page, size, Sort.by("aufgabenGruppe.name")));
|
||||
List<AufgabenGruppe> dtos = dbPage.getContent().stream()
|
||||
.map(a -> enrich(a.getAufgabenGruppe(), user.getUserId(), true))
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(manualPage(dtos, page, size));
|
||||
AufgabenGruppePage result = new AufgabenGruppePage();
|
||||
result.setContent(dtos);
|
||||
result.setCurrentPage(dbPage.getNumber());
|
||||
result.setTotalPages(dbPage.getTotalPages());
|
||||
result.setTotalElements(dbPage.getTotalElements());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── Entdecken (nur vanilla-safe Gruppen von anderen) ──
|
||||
@@ -82,11 +86,19 @@ public class VanillaAboController {
|
||||
List<AufgabenGruppe> dtos = gruppeRepository
|
||||
.findVanillaSafePublicFromOthers(user.getUserId(), namePattern).stream()
|
||||
.map(g -> enrich(g, user.getUserId(), aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), g)))
|
||||
.sorted(Comparator.comparingLong(AufgabenGruppe::getSubscriberCount).reversed()
|
||||
.sorted(java.util.Comparator.comparingLong(AufgabenGruppe::getSubscriberCount).reversed()
|
||||
.thenComparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(manualPage(dtos, page, size));
|
||||
int total = dtos.size();
|
||||
int start = page * size;
|
||||
List<AufgabenGruppe> content = start >= total ? List.of() : dtos.subList(start, Math.min(start + size, total));
|
||||
AufgabenGruppePage discoverPage = new AufgabenGruppePage();
|
||||
discoverPage.setContent(content);
|
||||
discoverPage.setCurrentPage(page);
|
||||
discoverPage.setTotalPages(total == 0 ? 1 : (int) Math.ceil((double) total / size));
|
||||
discoverPage.setTotalElements(total);
|
||||
return ResponseEntity.ok(discoverPage);
|
||||
}
|
||||
|
||||
// ── Abonnieren (nur vanilla-safe) ──
|
||||
@@ -138,16 +150,5 @@ public class VanillaAboController {
|
||||
return g;
|
||||
}
|
||||
|
||||
private AufgabenGruppePage manualPage(List<AufgabenGruppe> all, int page, int size) {
|
||||
int total = all.size();
|
||||
int start = page * size;
|
||||
List<AufgabenGruppe> content = start >= total ? List.of() : all.subList(start, Math.min(start + size, total));
|
||||
AufgabenGruppePage result = new AufgabenGruppePage();
|
||||
result.setContent(content);
|
||||
result.setCurrentPage(page);
|
||||
result.setTotalPages(total == 0 ? 1 : (int) Math.ceil((double) total / size));
|
||||
result.setTotalElements(total);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -84,18 +84,17 @@ public class VanillaAufgabenGruppeController {
|
||||
Principal principal) {
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
// Only vanilla-safe user groups (no Strafen, no Sperren)
|
||||
UUID userId = user.getUserId();
|
||||
String searchPattern = null;
|
||||
java.util.List<AufgabenGruppeEntity> all = gruppeRepository.listVanillaSafeWithUserAndSearch(
|
||||
userId, searchPattern, PageRequest.of(0, 500, Sort.by("name")));
|
||||
java.util.List<AufgabenGruppeEntity> ownOnly = all.stream()
|
||||
.filter(g -> userId.equals(g.getUserId())).toList();
|
||||
AufgabenGruppePage result = manualPage(ownOnly.stream().map(entity -> {
|
||||
Page<AufgabenGruppeEntity> dbPage = gruppeRepository.findByUserIdWithContent(
|
||||
user.getUserId(), PageRequest.of(page, size, Sort.by("name")));
|
||||
AufgabenGruppePage result = new AufgabenGruppePage();
|
||||
result.setContent(dbPage.getContent().stream().map(entity -> {
|
||||
AufgabenGruppe g = entity.toAufgabenGruppe();
|
||||
g.setSubscriberCount(aboRepository.countByAufgabenGruppe(entity));
|
||||
return g;
|
||||
}).toList(), page, DEFAULT_PAGE_SIZE);
|
||||
}).toList());
|
||||
result.setCurrentPage(dbPage.getNumber());
|
||||
result.setTotalPages(dbPage.getTotalPages());
|
||||
result.setTotalElements(dbPage.getTotalElements());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@@ -103,16 +102,14 @@ public class VanillaAufgabenGruppeController {
|
||||
public ResponseEntity<AufgabenGruppePage> listSystem(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size) {
|
||||
// Only vanilla-safe system groups (userId IS NULL → no strafen/sperren anyway, but filter for safety)
|
||||
Page<AufgabenGruppeEntity> result = gruppeRepository.findByUserIdIsNull(
|
||||
Page<AufgabenGruppeEntity> dbPage = gruppeRepository.findSystemGroupsWithContent(
|
||||
PageRequest.of(page, size, Sort.by("name")));
|
||||
AufgabenGruppePage r = new AufgabenGruppePage();
|
||||
r.setContent(result.getContent().stream()
|
||||
.filter(g -> g.getStrafen().isEmpty() && g.getSperren().isEmpty())
|
||||
r.setContent(dbPage.getContent().stream()
|
||||
.map(AufgabenGruppeEntity::toAufgabenGruppe).toList());
|
||||
r.setCurrentPage(result.getNumber());
|
||||
r.setTotalPages(result.getTotalPages());
|
||||
r.setTotalElements(result.getTotalElements());
|
||||
r.setCurrentPage(dbPage.getNumber());
|
||||
r.setTotalPages(dbPage.getTotalPages());
|
||||
r.setTotalElements(dbPage.getTotalElements());
|
||||
return ResponseEntity.ok(r);
|
||||
}
|
||||
|
||||
@@ -270,15 +267,4 @@ public class VanillaAufgabenGruppeController {
|
||||
return userService.requireUser(principal);
|
||||
}
|
||||
|
||||
private AufgabenGruppePage manualPage(java.util.List<AufgabenGruppe> all, int page, int size) {
|
||||
int total = all.size();
|
||||
int start = page * size;
|
||||
java.util.List<AufgabenGruppe> content = start >= total ? java.util.List.of() : all.subList(start, Math.min(start + size, total));
|
||||
AufgabenGruppePage result = new AufgabenGruppePage();
|
||||
result.setContent(content);
|
||||
result.setCurrentPage(page);
|
||||
result.setTotalPages(total == 0 ? 1 : (int) Math.ceil((double) total / size));
|
||||
result.setTotalElements(total);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import de.oaa.xxx.games.vanilla.entity.VanillaEinladungEntity;
|
||||
import de.oaa.xxx.games.vanilla.entity.VanillaEinladungEntity.Status;
|
||||
import de.oaa.xxx.games.vanilla.repository.VanillaEinladungRepository;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.social.repository.FriendshipRepository;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
@@ -106,13 +105,7 @@ public class VanillaEinladungController {
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
einladungRepository.save(entity);
|
||||
|
||||
String inviterName = userRepository.findById(inviterId).map(u -> u.getName()).orElse("Jemand");
|
||||
systemMessageService.send(
|
||||
inviterId, req.inviteeId(),
|
||||
inviterName + " hat dich zum Vanilla Game eingeladen.",
|
||||
"/community/einladungen.html",
|
||||
MessageCause.INVITATION
|
||||
);
|
||||
systemMessageService.pushInvitationUpdate(req.inviteeId());
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("einladungId", entity.getEinladungId());
|
||||
@@ -127,10 +120,7 @@ public class VanillaEinladungController {
|
||||
if (e == null) return ResponseEntity.notFound().build();
|
||||
if (!e.getInviterId().equals(userId)) return ResponseEntity.status(403).build();
|
||||
e.setStatus(Status.CANCELLED);
|
||||
String inviterName = userRepository.findById(userId).map(u -> u.getName()).orElse("Jemand");
|
||||
systemMessageService.send(userId, e.getInviteeId(),
|
||||
inviterName + " hat die Vanilla-Spieleinladung zurückgezogen.",
|
||||
"/community/einladungen.html", MessageCause.INVITATION);
|
||||
systemMessageService.pushInvitationUpdate(e.getInviteeId());
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ public class SystemMessageService {
|
||||
.findByUserIdAndCause(receiverId, cause)
|
||||
.orElseGet(() -> NotificationPreferenceEntity.defaultFor(receiverId, cause));
|
||||
|
||||
// FRIENDREQUEST ist immer in-app, unabhängig von der Einstellung
|
||||
boolean sendInApp = cause == MessageCause.FRIENDREQUEST || pref.isInApp();
|
||||
// FRIENDREQUEST und INVITATION sind immer nur in-app, kein E-Mail
|
||||
boolean sendInApp = cause == MessageCause.FRIENDREQUEST || cause == MessageCause.INVITATION || pref.isInApp();
|
||||
|
||||
if (sendInApp) {
|
||||
MessageEntity msg = new MessageEntity();
|
||||
@@ -76,7 +76,7 @@ public class SystemMessageService {
|
||||
sseService.push(receiverId, "NOTIFICATION", Map.of("unreadCount", unread, "text", text));
|
||||
}
|
||||
|
||||
if (pref.isEmail()) {
|
||||
if (pref.isEmail() && cause != MessageCause.INVITATION) {
|
||||
userRepository.findById(receiverId).ifPresent(user -> {
|
||||
try {
|
||||
Email email = new Email();
|
||||
@@ -91,6 +91,15 @@ public class SystemMessageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Benachrichtigt den Empfänger per SSE, dass sich seine Einladungsliste geändert hat,
|
||||
* ohne eine In-App-Nachricht oder E-Mail zu erstellen.
|
||||
*/
|
||||
public void pushInvitationUpdate(UUID receiverId) {
|
||||
if (receiverId == null) return;
|
||||
sseService.push(receiverId, "INVITATION", java.util.Map.of());
|
||||
}
|
||||
|
||||
private String causeTitel(MessageCause cause) {
|
||||
return switch (cause) {
|
||||
case INVITATION -> "XXX The Game – Neue Einladung";
|
||||
|
||||
588
xxxthegame/src/main/resources/sql/testdata_aufgabengruppen.sql
Normal file
@@ -0,0 +1,588 @@
|
||||
-- ============================================================
|
||||
-- Testdaten: Aufgabengruppen (generiert aus DefaultFiller)
|
||||
-- Toys und *Toy-Join-Tabellen werden ignoriert.
|
||||
-- UUID-Speicherung: varchar(36) als plain UUID-String
|
||||
-- Spaltennamen: SpringPhysicalNamingStrategy → snake_case
|
||||
-- ============================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ── Aufgabengruppen ──────────────────────────────────────────
|
||||
INSERT IGNORE INTO aufgaben_gruppe (gruppen_id, name, beschreibung, user_id, private_gruppe, bild, von) VALUES
|
||||
('10000000-0000-0000-0000-000000000001', 'Keuschhaltung weiblich', 'Enthält verschiedene Aufgaben für Keuschhaltung von weiblichen Spielpartnern', NULL, 0, NULL, NULL),
|
||||
('10000000-0000-0000-0000-000000000002', 'Keuschhaltung männlich', 'Enthält verschiedene Aufgaben für Keuschhaltung von männlichen Spielpartnern', NULL, 0, NULL, NULL),
|
||||
('10000000-0000-0000-0000-000000000003', 'Plugs', 'Enthält verschiedene Aufgaben für das Tragen von Buttplugs über einen gewissen Zeitraum.', NULL, 0, NULL, NULL),
|
||||
('10000000-0000-0000-0000-000000000004', 'Knebel', 'Enthält verschiedene Aufgaben für das Tragen von Knebeln über einen gewissen Zeitraum.', NULL, 0, NULL, NULL),
|
||||
('10000000-0000-0000-0000-000000000005', 'Strafen', 'Enthält verschiedene Bestrafungen', NULL, 0, NULL, NULL),
|
||||
('10000000-0000-0000-0000-000000000006', 'Aufgaben', 'Enthält verschiedene Sex-Aufgaben.', NULL, 0, NULL, NULL);
|
||||
|
||||
|
||||
-- ── Sperren ──────────────────────────────────────────────────
|
||||
-- Gruppe: Keuschhaltung weiblich
|
||||
INSERT IGNORE INTO sperre (sperre_id, kurz_text, text, release_text, minuten_von, minuten_bis, gruppe_id) VALUES
|
||||
('20000000-0000-0000-0000-000000000001', 'Voll-KG',
|
||||
'{PASSIV} trägt fortan einen Voll-KG, {AKTIV} ist der Keyholder',
|
||||
'{AKTIV}, es ist ab der Zeit {PASSIV} von ihrem KG zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000001'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000002', 'Voll-KG + Vaginaldildo',
|
||||
'{PASSIV} trägt fortan einen Voll-KG mit Vaginaldildo, {AKTIV} ist der Keyholder',
|
||||
'{AKTIV}, es ist ab der Zeit {PASSIV} von ihrem KG zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000001'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000003', 'Voll-KG + Analdildo',
|
||||
'{PASSIV} trägt fortan einen Voll-KG mit Analdildo, {AKTIV} ist der Keyholder',
|
||||
'{AKTIV}, es ist ab der Zeit {PASSIV} von ihrem KG zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000001'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000004', 'Voll-KG + Doubleplugged',
|
||||
'{PASSIV} trägt fortan einen Voll-KG mit Vaginal- und Analdildo, {AKTIV} ist der Keyholder',
|
||||
'{AKTIV}, es ist ab der Zeit {PASSIV} von ihrem KG zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000001');
|
||||
|
||||
-- Gruppe: Keuschhaltung männlich
|
||||
INSERT IGNORE INTO sperre (sperre_id, kurz_text, text, release_text, minuten_von, minuten_bis, gruppe_id) VALUES
|
||||
('20000000-0000-0000-0000-000000000005', 'Peniskäfig',
|
||||
'{PASSIV} trägt fortan einen Peniskäfig, {AKTIV} ist der Keyholder',
|
||||
'{AKTIV}, es ist ab der Zeit {PASSIV} von seinem Peniskäfig zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000002'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000006', 'Voll-KG',
|
||||
'{PASSIV} trägt fortan einen Voll-KG, {AKTIV} ist der Keyholder',
|
||||
'{AKTIV}, es ist ab der Zeit {PASSIV} von seinem KG zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000002'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000007', 'Voll-KG + Analdildo',
|
||||
'{PASSIV} trägt fortan einen Voll-KG mit Analdildo, {AKTIV} ist der Keyholder',
|
||||
'{AKTIV}, es ist ab der Zeit {PASSIV} von seinem KG zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000002');
|
||||
|
||||
-- Gruppe: Plugs
|
||||
INSERT IGNORE INTO sperre (sperre_id, kurz_text, text, release_text, minuten_von, minuten_bis, gruppe_id) VALUES
|
||||
('20000000-0000-0000-0000-000000000008', 'Plug klein',
|
||||
'{AKTIV} führt {PASSIV} einen kleinen Buttplug in anal ein, dieser ist bis auf weiteres zu tragen.',
|
||||
'{AKTIV}, es ist Zeit {PASSIV} von seinem Plug zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000003'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000009', 'Plug mittel',
|
||||
'{AKTIV} führt {PASSIV} einen mittelgroßen Buttplug anal ein, dieser ist bis auf weiteres zu tragen.',
|
||||
'{AKTIV}, es ist Zeit {PASSIV} von seinem Plug zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000003'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000010', 'Plug groß',
|
||||
'{AKTIV} führt {PASSIV} einen großen Buttplug anal ein, dieser ist bis auf weiteres zu tragen.',
|
||||
'{AKTIV}, es ist Zeit {PASSIV} von seinem Plug zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000003'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000011', 'Elektro-Plug anal',
|
||||
'{AKTIV} führt {PASSIV} einen Elekro-Plug anal ein, dieser ist bis auf weiteres zu tragen. {AKTIV} darf {PASSIV} leichte Stromstöße verpassen',
|
||||
'{AKTIV}, es ist Zeit {PASSIV} von seinem Plug zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000003'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000012', 'Elektro-Plug vaginal',
|
||||
'{AKTIV} führt {PASSIV} einen Elekto-Plug vaginal ein, dieser ist bis auf weiteres zu tragen. {AKTIV} darf {PASSIV} leichte Stromstöße verpassen',
|
||||
'{AKTIV}, es ist Zeit {PASSIV} von seinem Plug zu befreien',
|
||||
10, 30, '10000000-0000-0000-0000-000000000003');
|
||||
|
||||
-- Gruppe: Knebel
|
||||
INSERT IGNORE INTO sperre (sperre_id, kurz_text, text, release_text, minuten_von, minuten_bis, gruppe_id) VALUES
|
||||
('20000000-0000-0000-0000-000000000013', 'Ballknebel',
|
||||
'{AKTIV}, lege {PASSIV} einen Ballknebel an, dieser ist bis auf weiteres zu tragen.',
|
||||
'{AKTIV}, es ist Zeit {PASSIV} von seinem Knebel zu befreien.',
|
||||
10, 30, '10000000-0000-0000-0000-000000000004'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000014', 'Penisknebel',
|
||||
'{AKTIV}, lege {PASSIV} einen Dildoknebel an, dieser ist bis auf weiteres zu tragen.',
|
||||
'{AKTIV}, es ist Zeit {PASSIV} von seinem Knebel zu befreien.',
|
||||
10, 30, '10000000-0000-0000-0000-000000000004'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000015', 'Aufblasbarer Knebel',
|
||||
'{AKTIV}, lege {PASSIV} einen aufblasbaren Knebel an und pumpe diesen soweit auf, dass {PASSIV} noch halbwegs gut atmen kann, dieser ist bis auf weiteres zu tragen.',
|
||||
'{AKTIV}, es ist Zeit {PASSIV} von seinem Knebel zu befreien.',
|
||||
5, 15, '10000000-0000-0000-0000-000000000004'),
|
||||
|
||||
('20000000-0000-0000-0000-000000000016', 'Isolationsmaske',
|
||||
'{AKTIV}, lege {PASSIV} eine Isolationsmaske an, diese ist bis auf weiteres zu tragen.',
|
||||
'{AKTIV}, es ist Zeit {PASSIV} von seinem Knebel zu befreien.',
|
||||
5, 15, '10000000-0000-0000-0000-000000000004');
|
||||
|
||||
-- sperre_sperre_fuer (war @CollectionTable name="sperre_sperreFuer" → snake_case)
|
||||
INSERT IGNORE INTO sperre_sperre_fuer (sperre_id, werkzeug) VALUES
|
||||
('20000000-0000-0000-0000-000000000001', 'VAGINA'),
|
||||
('20000000-0000-0000-0000-000000000002', 'VAGINA'),
|
||||
('20000000-0000-0000-0000-000000000003', 'VAGINA'),
|
||||
('20000000-0000-0000-0000-000000000003', 'ANUS'),
|
||||
('20000000-0000-0000-0000-000000000004', 'VAGINA'),
|
||||
('20000000-0000-0000-0000-000000000004', 'ANUS'),
|
||||
('20000000-0000-0000-0000-000000000005', 'PENIS'),
|
||||
('20000000-0000-0000-0000-000000000006', 'PENIS'),
|
||||
('20000000-0000-0000-0000-000000000007', 'PENIS'),
|
||||
('20000000-0000-0000-0000-000000000007', 'ANUS'),
|
||||
('20000000-0000-0000-0000-000000000008', 'ANUS'),
|
||||
('20000000-0000-0000-0000-000000000009', 'ANUS'),
|
||||
('20000000-0000-0000-0000-000000000010', 'ANUS'),
|
||||
('20000000-0000-0000-0000-000000000011', 'ANUS'),
|
||||
('20000000-0000-0000-0000-000000000012', 'VAGINA'),
|
||||
('20000000-0000-0000-0000-000000000013', 'MUND'),
|
||||
('20000000-0000-0000-0000-000000000014', 'MUND'),
|
||||
('20000000-0000-0000-0000-000000000015', 'MUND'),
|
||||
('20000000-0000-0000-0000-000000000016', 'MUND');
|
||||
|
||||
|
||||
-- ── Strafen ──────────────────────────────────────────────────
|
||||
INSERT IGNORE INTO strafe (strafe_id, kurz_text, text, level, sekunden_von, sekunden_bis, gruppe_id) VALUES
|
||||
('30000000-0000-0000-0000-000000000001', '5 Schläge mit flachen Hand',
|
||||
'{PASSIV} stellt sich mit dem Gesicht zur Wand, Hände hinterm Kopf, Beine schulterbreit, {AKTIV} verpasst {PASSIV} 5 Schläge mit der flachen Hand auf das Gesäß.',
|
||||
1, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000002', '15 Schläge mit flachen Hand',
|
||||
'{PASSIV} stellt sich mit dem Gesicht zur Wand, Hände hinterm Kopf, Beine schulterbreit, {AKTIV} verpasst {PASSIV} 15 beherzte Schläge mit der flachen Hand auf das Gesäß, {PASSIV} zählt laut mit',
|
||||
3, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000003', '5 Schläge mit Gerte',
|
||||
'{PASSIV} stellt sich mit dem Gesicht zur Wand, Hände hinterm Kopf, Beine schulterbreit, {AKTIV} verpasst {PASSIV} 5 Schläge mit der Gerte auf das Gesäß.',
|
||||
2, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000004', '15 Schläge mit Gerte',
|
||||
'{PASSIV} stellt sich mit dem Gesicht zur Wand, Hände hinterm Kopf, Beine schulterbreit, {AKTIV} verpasst {PASSIV} 15 beherzte Schläge mit der Gerte auf das Gesäß, {PASSIV} zählt laut mit',
|
||||
4, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000005', '5 Schläge mit Paddel',
|
||||
'{PASSIV} stellt sich mit dem Gesicht zur Wand, Hände hinterm Kopf, Beine schulterbreit, {AKTIV} verpasst {PASSIV} 5 Schläge mit dem Paddel auf das Gesäß.',
|
||||
2, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000006', '15 Schläge mit Paddel',
|
||||
'{PASSIV} stellt sich mit dem Gesicht zur Wand, Hände hinterm Kopf, Beine schulterbreit, {AKTIV} verpasst {PASSIV} 15 beherzte Schläge mit dem Paddel auf das Gesäß, {PASSIV} zählt laut mit',
|
||||
4, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000007', '5 Schläge mit Peitsche',
|
||||
'{PASSIV} stellt sich mit dem Gesicht zur Wand, Hände hinterm Kopf, Beine schulterbreit, {AKTIV} verpasst {PASSIV} 5 Schläge mit der Peitsche auf das Gesäß.',
|
||||
3, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000008', '15 Schläge mit Peitsche',
|
||||
'{PASSIV} stellt sich mit dem Gesicht zur Wand, Hände hinterm Kopf, Beine schulterbreit, {AKTIV} verpasst {PASSIV} 15 beherzte Schläge mit der Peitsche auf das Gesäß, {PASSIV} zählt laut mit',
|
||||
5, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000009', 'Schläge auf Klitoris mit Hand',
|
||||
'{PASSIV} liegt auf dem Rücken mit breiten Beinen, {AKTIV} verpasst {PASSIV} 5 Schläge mit der Hand auf die Klitoris, {PASSIV} zählt laut mit',
|
||||
4, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000010', 'Schläge auf Klitoris mit Peitsche',
|
||||
'{PASSIV} liegt auf dem Rücken mit breiten Beinen, {AKTIV} verpasst {PASSIV} 5 Schläge mit der Peitsche auf die Klitoris, {PASSIV} zählt laut mit',
|
||||
5, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000011', 'Schläge auf Klitoris mit Paddel',
|
||||
'{PASSIV} liegt auf dem Rücken mit breiten Beinen, {AKTIV} verpasst {PASSIV} 5 Schläge mit dem Paddel auf die Klitoris, {PASSIV} zählt laut mit',
|
||||
5, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000012', 'Schläge auf Klitoris mit Gerte',
|
||||
'{PASSIV} liegt auf dem Rücken mit breiten Beinen, {AKTIV} verpasst {PASSIV} 5 Schläge mit der Gerte auf die Klitoris, {PASSIV} zählt laut mit',
|
||||
5, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000013', '5 Ohrfeigen',
|
||||
'{PASSIV} stellt sich mit dem Rücken zur Wand, Hände hinterm Kopf, Beine schulterbreit, {AKTIV} verpasst {PASSIV} 5 Ohrfeigen, {PASSIV} zählt laut mit',
|
||||
5, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000014', 'Elektroplug anal',
|
||||
'{AKTIV} führt {PASSIV} anal einen Elektro-Plug ein. {AKTIV} erhöht ganz langsam die Intensität bis {PASSIV} ''STOP'' sagt, dann fängt {AKTIV} wieder bei null an',
|
||||
5, 30, 90, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000015', 'Elektroplug vaginal',
|
||||
'{AKTIV} führt {PASSIV} vaginal einen Elektro-Plug ein. {AKTIV} erhöht ganz langsam die Intensität bis {PASSIV} ''STOP'' sagt, dann fängt {AKTIV} wieder bei null an',
|
||||
5, 30, 90, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000016', 'Pumpplug anal',
|
||||
'{AKTIV} führt {PASSIV} anal einen Pump-Plug ein. {AKTIV} pumpt ganz langsam auf bis {PASSIV} ''STOP'' sagt, dann fängt {AKTIV} wieder bei null an',
|
||||
5, 30, 90, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000017', 'Pumpplug vaginal',
|
||||
'{AKTIV} führt {PASSIV} vaginal einen Pump-Plug ein. {AKTIV} pumpt ganz langsam auf bis {PASSIV} ''STOP'' sagt, dann fängt {AKTIV} wieder bei null an',
|
||||
5, 30, 90, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000018', 'Facesitting (Vagina)',
|
||||
'{PASSIV} liegt auf dem Rücken, {AKTIV} setzt sich auf das Gesicht von {PASSIV} und lässt sich den Vaginal und/oder Analbereich verwöhnen',
|
||||
2, 90, 180, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000019', 'Facesitting gefesselt (Vagina)',
|
||||
'{PASSIV} liegt mit auf den Rücken gefesselten Händen auf dem Rücken, {AKTIV} setzt sich auf das Gesicht von {PASSIV} und lässt sich den Vaginal und/oder Analbereich verwöhnen',
|
||||
4, 90, 180, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000020', 'Facesitting (Penis)',
|
||||
'{PASSIV} liegt auf dem Rücken, {AKTIV} setzt sich auf das Gesicht von {PASSIV} und lässt sich den Penis und/oder Analbereich verwöhnen',
|
||||
2, 90, 180, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000021', 'Facesitting gefesselt (Penis)',
|
||||
'{PASSIV} liegt mit auf den Rücken gefesselten Händen auf dem Rücken, {AKTIV} setzt sich auf das Gesicht von {PASSIV} und lässt sich den Penis und/oder Analbereich verwöhnen',
|
||||
4, 90, 180, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000022', 'Facesitting Doppelpenisknebel',
|
||||
'{PASSIV} liegt auf dem Rücken, {AKTIV} legt {PASSIV} einen Doppel-Penisknebel an und reitet diesen vaginal oder anal',
|
||||
3, 60, 120, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000023', 'Facesitting Doppelpenisknebel gefesselt',
|
||||
'{PASSIV} liegt mit auf den Rücken gefesselten Händen auf dem Rücken, {AKTIV} legt {PASSIV} einen Doppel-Penisknebel an und reitet diesen vaginal oder anal',
|
||||
3, 60, 120, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000024', 'Nippelklemmen',
|
||||
'{AKTIV} legt {PASSIV} Nippelklemmen an, {AKTIV} zieht an der Kette und erhöht ganz langsam die Intensität bis {PASSIV} ''STOP'' sagt, dann fängt {AKTIV} wieder bei null an',
|
||||
3, 30, 90, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000025', 'Nippelbehandlung',
|
||||
'{AKTIV} nimmt die Nippel von {PASSIV} zwischen die Finger und erhöht langsam den Druck bis {PASSIV} ''STOP'' sagt',
|
||||
2, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000026', 'Hilflos liegen lassen',
|
||||
'{AKTIV} fesselt, knebelt und verbindet die Augen von {PASSIV}. {AKTIV} lässt {PASSIV} wehrlos liegen, bei Ablauf der Zeit erlöst {AKTIV} {PASSIV} mit einem beherzten Platsch auf den Po',
|
||||
4, 300, 600, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000027', 'Strapon reiten',
|
||||
'{PASSIV} liegt auf dem Rücken und trägt dabei einen Umschnalldildo. {AKTIV} reitet den Umschnalldildo von {PASSIV}',
|
||||
3, 60, 180, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000028', 'Strapon reiten gefesselt',
|
||||
'{AKTIV} fesselt und knebelt {PASSIV}. {PASSIV} trägt dabei einen Umschnalldildo. {AKTIV} reitet den Umschnalldildo von {PASSIV}',
|
||||
4, 60, 180, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000029', 'Teaseblowjob mit dem Strapon',
|
||||
'{AKTIV} fesselt und knebelt {PASSIV}. {PASSIV} trägt dabei einen Umschnalldildo, KG und einen großen Buttplug. {AKTIV} gibt dem Umschnalldildo einen Blowjob in 69er Position und präsentiert {PASSIV} dabei den Intimbereich',
|
||||
5, 180, 300, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000030', 'Teasereiten mit Strapon',
|
||||
'{AKTIV} fesselt und knebelt {PASSIV}. {PASSIV} trägt dabei einen Umschnalldildo, KG und einen großen Buttplug. {AKTIV} reitet den Umschnalldildo von {PASSIV}.',
|
||||
5, 180, 300, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000031', 'Tease mit Selbstbefriedigung (Mann KG)',
|
||||
'{AKTIV} knebelt und fesselt {PASSIV} an einen Stuhl. {PASSIV} trägt dabei einen KG und einen großen Buttplug. {AKTIV} befriedigt sich dann vor den Augen von {PASSIV} selber',
|
||||
4, 240, 360, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000032', 'Tease mit Selbstbefriedigung (Frau KG)',
|
||||
'{AKTIV} knebelt und fesselt {PASSIV} an einen Stuhl. {PASSIV} trägt dabei einen KG und einen großen Buttplug. {AKTIV} befriedigt sich dann vor den Augen von {PASSIV} selber',
|
||||
4, 240, 360, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000033', 'Blowjob auf allen vieren',
|
||||
'{AKTIV}, zwinge {PASSIV} vor dir auf die Knie, führe dein Glied (oder Strap on) in den Mund von {PASSIV} ein und zeig mit einem Deepthroat, wer das sagen hat',
|
||||
5, 30, 90, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000034', 'Oralsex mit kleinem Dildo in der Vagina',
|
||||
'{PASSIV}, geh auf die Knie und reite vaginal einen kleinen Dildo, befriedige dabei {AKTIV} oral.',
|
||||
2, 60, 120, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000035', 'Oralsex mit großen Dildo in der Vagina',
|
||||
'{PASSIV}, geh auf die Knie und reite vaginal einen großen Dildo, befriedige dabei {AKTIV} oral.',
|
||||
4, 60, 120, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000036', 'Oralsex mit kleinem Dildo im Anus',
|
||||
'{PASSIV}, geh auf die Knie und reite anal einen kleinen Dildo, befriedige dabei {AKTIV} oral.',
|
||||
3, 60, 120, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000037', 'Oralsex mit großen Dildo im Anus',
|
||||
'{PASSIV}, geh auf die Knie und reite anal einen großen Dildo, befriedige dabei {AKTIV} oral.',
|
||||
4, 60, 120, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000038', 'Vagina dehnen',
|
||||
'{PASSIV} geht auf alle viere und streckt den Hintern schön in die Luft, {AKTIV} führe langsam nach und nach mehr Finger in die Vagina von {PASSIV} ein, bis {PASSIV} ''STOP'' sagt',
|
||||
2, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000039', 'Anus dehnen',
|
||||
'{PASSIV} geht auf alle viere und streckt den Hintern schön in die Luft, {AKTIV} führe langsam nach und nach mehr Finger in die Anus von {PASSIV} ein, bis {PASSIV} ''STOP'' sagt',
|
||||
2, NULL, NULL, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000040', 'Vaginalsex in Missionarstellung und Breathplay',
|
||||
'{AKTIV} dringt in Missionarsstellung in {PASSIV} und gibt vollgas, dabei packt {AKTIV} {PASSIV} am Hals und drückt beherzt zu',
|
||||
4, 30, 60, '10000000-0000-0000-0000-000000000005'),
|
||||
|
||||
('30000000-0000-0000-0000-000000000041', 'Analsex in Missionarstellung und Breathplay',
|
||||
'{AKTIV} dringt in Missionarsstellung anal in {PASSIV} und gibt vollgas, dabei packt {AKTIV} {PASSIV} am Hals und drückt beherzt zu',
|
||||
4, 30, 60, '10000000-0000-0000-0000-000000000005');
|
||||
|
||||
-- strafe_benoetigt_passiv (war @CollectionTable name="strafe_benoetigtPassiv")
|
||||
INSERT IGNORE INTO strafe_benoetigt_passiv (strafe_id, werkzeug) VALUES
|
||||
('30000000-0000-0000-0000-000000000009', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000010', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000011', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000012', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000014', 'ANUS'),
|
||||
('30000000-0000-0000-0000-000000000015', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000016', 'ANUS'),
|
||||
('30000000-0000-0000-0000-000000000017', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000018', 'MUND'),
|
||||
('30000000-0000-0000-0000-000000000019', 'MUND'),
|
||||
('30000000-0000-0000-0000-000000000020', 'MUND'),
|
||||
('30000000-0000-0000-0000-000000000021', 'MUND'),
|
||||
('30000000-0000-0000-0000-000000000022', 'MUND'),
|
||||
('30000000-0000-0000-0000-000000000023', 'MUND'),
|
||||
('30000000-0000-0000-0000-000000000033', 'MUND'),
|
||||
('30000000-0000-0000-0000-000000000034', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000035', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000036', 'ANUS'),
|
||||
('30000000-0000-0000-0000-000000000037', 'ANUS'),
|
||||
('30000000-0000-0000-0000-000000000038', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000039', 'ANUS'),
|
||||
('30000000-0000-0000-0000-000000000040', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000041', 'ANUS');
|
||||
|
||||
-- strafe_benoetigt_aktiv (war @CollectionTable name="strafe_benoetigtAktiv")
|
||||
INSERT IGNORE INTO strafe_benoetigt_aktiv (strafe_id, werkzeug) VALUES
|
||||
('30000000-0000-0000-0000-000000000018', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000018', 'ANUS'),
|
||||
('30000000-0000-0000-0000-000000000019', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000019', 'ANUS'),
|
||||
('30000000-0000-0000-0000-000000000020', 'PENIS'),
|
||||
('30000000-0000-0000-0000-000000000020', 'ANUS'),
|
||||
('30000000-0000-0000-0000-000000000021', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000021', 'PENIS'),
|
||||
('30000000-0000-0000-0000-000000000022', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000023', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000027', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000027', 'ANUS'),
|
||||
('30000000-0000-0000-0000-000000000028', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000028', 'ANUS'),
|
||||
('30000000-0000-0000-0000-000000000029', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000030', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000031', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000032', 'PENIS'),
|
||||
('30000000-0000-0000-0000-000000000033', 'PENIS'),
|
||||
('30000000-0000-0000-0000-000000000033', 'UMSCHNALLDILDO'),
|
||||
('30000000-0000-0000-0000-000000000034', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000034', 'PENIS'),
|
||||
('30000000-0000-0000-0000-000000000035', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000035', 'PENIS'),
|
||||
('30000000-0000-0000-0000-000000000036', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000036', 'PENIS'),
|
||||
('30000000-0000-0000-0000-000000000037', 'VAGINA'),
|
||||
('30000000-0000-0000-0000-000000000037', 'PENIS'),
|
||||
('30000000-0000-0000-0000-000000000040', 'PENIS'),
|
||||
('30000000-0000-0000-0000-000000000040', 'UMSCHNALLDILDO'),
|
||||
('30000000-0000-0000-0000-000000000041', 'PENIS'),
|
||||
('30000000-0000-0000-0000-000000000041', 'UMSCHNALLDILDO');
|
||||
|
||||
|
||||
-- ── Aufgaben ─────────────────────────────────────────────────
|
||||
INSERT IGNORE INTO aufgabe (aufgabe_id, kurz_text, text, level, sekunden_von, sekunden_bis, gruppe_id) VALUES
|
||||
('40000000-0000-0000-0000-000000000001', 'Hintern präsentieren',
|
||||
'{AKTIV}, zeig {PASSIV} deinen Hintern, gib dir selber dabei ein oder zwei Klappse auf den Po',
|
||||
1, NULL, NULL, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000002', 'Hals küssen',
|
||||
'{AKTIV}, küsse den Hals von {PASSIV} leidenschaftlich',
|
||||
1, 30, 60, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000003', 'Bauchnabel küssen',
|
||||
'{AKTIV}, zeichne mit Küssen den Bauchnabel von {PASSIV} nach',
|
||||
1, 30, 60, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000004', 'Ohren knabbern',
|
||||
'{AKTIV}, knabber leidenschaftlich an den Ohrläppchen von {PASSIV}',
|
||||
1, 30, 60, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000005', 'Berühren ohne anfassen',
|
||||
'{AKTIV}, berühre den gesamten Körper von {PASSIV} ohne die Hände zu verwenden',
|
||||
2, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000006', 'Nacken küssen',
|
||||
'{PASSIV} sitzt vor {AKTIV}, {AKTIV} küsste leidenschaftlich den Nacken von {PASSIV}',
|
||||
1, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000007', 'Brust küssen',
|
||||
'{AKTIV}, küsse die Brust von {PASSIV} ohne die Nippel zu berühren',
|
||||
1, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000008', 'Nippel verwöhnen',
|
||||
'{AKTIV}, verwöhne die Nippel von {PASSIV} mit Küssen',
|
||||
2, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000009', 'Hintern küssen',
|
||||
'{AKTIV}, küsse den Hintern von {PASSIV} ohne den Anus zu berühren',
|
||||
1, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000010', 'Intimkuss durch Unterwäsche',
|
||||
'{AKTIV}, küsse den Intimbereich von {PASSIV} durch die Unterwäsche',
|
||||
2, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000011', 'Brustmassage',
|
||||
'{AKTIV}, massiere die Brust von {PASSIV} leidenschaftlich',
|
||||
1, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000012', 'Hinternmassage',
|
||||
'{AKTIV}, massiere den Hintern von {PASSIV} leidenschaftlich',
|
||||
1, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000013', 'Rückenmassage',
|
||||
'{AKTIV}, massiere den Rücken von {PASSIV} leidenschaftlich',
|
||||
1, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000014', 'Oberschenkelmassage',
|
||||
'{AKTIV}, massiere die Oberschenkel von {PASSIV} leidenschaftlich',
|
||||
1, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000015', 'Klitoris mit Vibrator verwöhnen',
|
||||
'{AKTIV}, verwöhne die Klitoris von {PASSIV} mit einem Vibrator',
|
||||
3, 30, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000016', 'Cunnilingus und Finger in Vagina',
|
||||
'{AKTIV}, verwöhne die Klitoris von {PASSIV} mit dem Mund, führe dabei einen bis zwei Finger in die Vagina von {PASSIV} ein',
|
||||
3, 30, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000017', 'Klitoris mit Fingern verwöhnen und Finger in Vagina',
|
||||
'{AKTIV}, verwöhne die Klitoris von {PASSIV} mit der Hand, führe dabei einen bis zwei Finger in die Vagina von {PASSIV} ein',
|
||||
4, 30, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000018', 'Eichel mit Vibrator verwöhnen',
|
||||
'{AKTIV}, verwöhne die Eichel von {PASSIV} mit einem Vibrator',
|
||||
3, 30, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000019', 'Felatio',
|
||||
'{AKTIV}, verwöhne die Eichel von {PASSIV} mit dem Mund',
|
||||
3, 30, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000020', 'Handjob',
|
||||
'{AKTIV}, verwöhne die Eichel von {PASSIV} mit der Hand',
|
||||
3, 30, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000021', 'Facesitting',
|
||||
'{AKTIV} liegt auf dem Rücken, {PASSIV} sitzt auf seinem Gesicht. {AKTIV}, verwöhne die Vagina von {PASSIV} mit dem Mund',
|
||||
4, 60, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000022', '69er-Position',
|
||||
'69er-Zeit: {AKTIV} liegt oben. {PASSIV}, falls du verschlossen bist, ziehe einen Strap on an, damit {AKTIV} auch was zu tun hat.',
|
||||
4, 60, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000023', 'Kleiner Dildo vaginal',
|
||||
'{AKTIV}, führe {PASSIV} einen kleinen Dildo vaginal ein und verwöhne {PASSIV} durch langsame Bewegungen mit selbigem',
|
||||
3, 30, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000024', 'Großer Dildo vaginal',
|
||||
'{AKTIV}, führe {PASSIV} einen großen Dildo vaginal ein und verwöhne {PASSIV} durch langsame Bewegungen mit selbigem',
|
||||
4, 30, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000025', 'Großer Dildo vaginal schnell',
|
||||
'{AKTIV}, führe {PASSIV} einen großen Dildo vaginal ein und bewege selbigen möglichst schnell rein und raus',
|
||||
5, 30, 60, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000026', 'Missionarstellung langsam',
|
||||
'{AKTIV} dringt in Missionarstellung in {PASSIV} ein und verwöhnt {PASSIV} mit langsamen Bewegungen',
|
||||
3, 60, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000027', 'Missionarstellung schnell',
|
||||
'{AKTIV} dringt in Missionarstellung in {PASSIV} ein und verwöhnt {PASSIV} mit schnellen Bewegungen',
|
||||
4, 30, 90, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000028', 'Missionarstellung Vollgas',
|
||||
'{AKTIV} dringt in Missionarstellung in {PASSIV} ein und gibt vollgas',
|
||||
5, 30, 60, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000029', 'Reiterstellung langsam',
|
||||
'{PASSIV} setzt sich in Reiterstellung auf {AKTIV}. {PASSIV} bestimmt das Tempo',
|
||||
3, 60, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000030', 'Reiterstellung schnell',
|
||||
'{PASSIV} setzt sich in Reiterstellung auf {AKTIV}. {PASSIV} versucht das Tempo hoch zu halten',
|
||||
4, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000031', 'Reiterstellung vollgas',
|
||||
'{PASSIV} setzt sich in Reiterstellung auf {AKTIV} und gibt vollgas',
|
||||
5, 30, 60, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000032', 'Doggystyle langsam',
|
||||
'{AKTIV} dringt in Hundestellung in {PASSIV} ein und verwöhnt {PASSIV} mit langsamen Bewegungen',
|
||||
3, 60, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000033', 'Doggystyle schnell',
|
||||
'{AKTIV} dringt in Hundestellung in {PASSIV} ein und verwöhnt {PASSIV} mit schnellen Bewegungen',
|
||||
4, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000034', 'Doggystyle vollgas',
|
||||
'{AKTIV} dringt in Hundestellung in {PASSIV} ein und gibt vollgas',
|
||||
5, 30, 60, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000035', 'Doggystyle vollgas keinen Mucks',
|
||||
'{AKTIV} dringt in Hundestellung in {PASSIV} ein und gibt vollgas. {PASSIV} darf dabei keinen Laut von sich geben.',
|
||||
5, 30, 60, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000036', 'Doggystyle Tempo bestimmt die ''gefickte'' Person',
|
||||
'{AKTIV} dringt in Hundestellung in {PASSIV} ein. {AKTIV} hält still und {PASSIV} gibt das Tempo vor',
|
||||
3, 60, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000037', 'Löffelchen langsam',
|
||||
'{AKTIV} dringt in Löffelchenstellung in {PASSIV} ein und verwöhnt {PASSIV} mit langsamen Bewegungen',
|
||||
3, 60, 180, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000038', 'Löffelchen schnell',
|
||||
'{AKTIV} dringt in Löffelchenstellung in {PASSIV} ein und verwöhnt {PASSIV} mit schnellen Bewegungen',
|
||||
4, 60, 120, '10000000-0000-0000-0000-000000000006'),
|
||||
|
||||
('40000000-0000-0000-0000-000000000039', 'Löffelchen vollgas',
|
||||
'{AKTIV} dringt in Löffelchenstellung in {PASSIV} ein und gibt vollgas',
|
||||
5, 30, 60, '10000000-0000-0000-0000-000000000006');
|
||||
|
||||
-- aufgabe_benoetigt_aktiv (war @CollectionTable name="aufgabe_benoetigtAktiv")
|
||||
INSERT IGNORE INTO aufgabe_benoetigt_aktiv (aufgabe_id, werkzeug) VALUES
|
||||
('40000000-0000-0000-0000-000000000002', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000003', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000004', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000006', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000007', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000008', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000009', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000010', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000016', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000019', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000021', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000022', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000022', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000026', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000026', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000027', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000027', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000028', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000028', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000029', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000029', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000030', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000030', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000031', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000031', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000032', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000032', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000033', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000033', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000034', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000034', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000035', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000035', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000036', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000036', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000037', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000037', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000038', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000038', 'UMSCHNALLDILDO'),
|
||||
('40000000-0000-0000-0000-000000000039', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000039', 'UMSCHNALLDILDO');
|
||||
|
||||
-- aufgabe_benoetigt_passiv (war @CollectionTable name="aufgabe_benoetigtPassiv")
|
||||
INSERT IGNORE INTO aufgabe_benoetigt_passiv (aufgabe_id, werkzeug) VALUES
|
||||
('40000000-0000-0000-0000-000000000015', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000016', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000017', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000018', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000019', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000020', 'PENIS'),
|
||||
('40000000-0000-0000-0000-000000000021', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000022', 'MUND'),
|
||||
('40000000-0000-0000-0000-000000000023', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000024', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000025', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000026', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000027', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000028', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000029', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000030', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000031', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000032', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000033', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000034', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000035', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000036', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000037', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000038', 'VAGINA'),
|
||||
('40000000-0000-0000-0000-000000000039', 'VAGINA');
|
||||
@@ -194,6 +194,15 @@
|
||||
font-size:0.78rem; color:var(--color-muted); line-height:1.6; }
|
||||
.placeholder-hint code { background:rgba(233,69,96,0.12); color:var(--color-primary);
|
||||
border-radius:3px; padding:0.05rem 0.3rem; font-size:0.75rem; }
|
||||
#iTextAC { position:fixed; z-index:9999; background:var(--color-surface,#1e1e2e);
|
||||
border:1px solid var(--color-border,#444); border-radius:6px;
|
||||
box-shadow:0 4px 14px rgba(0,0,0,.5); display:none; overflow:hidden; min-width:180px; max-height:280px; overflow-y:auto; }
|
||||
.ac-item { padding:0.45rem 0.9rem; cursor:pointer; font-size:0.88rem;
|
||||
font-family:monospace; color:var(--color-text,#cdd6f4); user-select:none; }
|
||||
.ac-item:hover, .ac-item-active { background:var(--color-primary,#cba6f7); color:#1e1e2e; }
|
||||
.ac-separator { padding:0.2rem 0.7rem; font-size:0.72rem; color:var(--color-muted);
|
||||
text-transform:uppercase; letter-spacing:0.05em; background:rgba(255,255,255,0.04);
|
||||
pointer-events:none; border-top:1px solid rgba(136,136,136,0.2); margin-top:2px; }
|
||||
.modal-two-col { display:flex; gap:0.75rem; }
|
||||
.modal-two-col > * { flex:1; }
|
||||
.werkzeug-checks { display:flex; flex-wrap:nowrap; gap:0.25rem; margin-top:0.5rem; }
|
||||
@@ -315,6 +324,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="iText" rows="4" maxlength="4000" placeholder="Ausführliche Beschreibung…"></textarea>
|
||||
<div id="iTextAC"></div>
|
||||
|
||||
<div id="iGeschlechtRow">
|
||||
<label>Geschlecht der Person die kommt *</label>
|
||||
@@ -430,6 +440,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Verschieben-Modal -->
|
||||
<div class="modal-backdrop" id="moveModal">
|
||||
<div class="modal" style="max-width:400px;">
|
||||
<h2>In andere Gruppe verschieben</h2>
|
||||
<p id="moveModalItemName" style="font-size:0.9rem;color:var(--color-muted);margin:0 0 1rem 0;"></p>
|
||||
<div class="form-group">
|
||||
<label for="moveModalSelect">Zielgruppe</label>
|
||||
<select id="moveModalSelect" class="form-control" style="height:2.6rem;font-size:1rem;"></select>
|
||||
</div>
|
||||
<p id="moveModalError" style="color:var(--color-danger);font-size:0.85rem;min-height:1.2em;margin:0.5rem 0 0 0;"></p>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" id="moveModalCancel">Abbrechen</button>
|
||||
<button class="btn-save" id="moveModalOk">Verschieben</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
|
||||
@@ -926,7 +953,7 @@ function renderAdminGruppen(gruppen) {
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId)}
|
||||
${renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), 'strafe', renderStrafe, g.gruppenId)}
|
||||
${renderSubSection('Zeitstrafen', sortByName(g.sperren || []), 'zeitstrafe', renderZeitstrafe, g.gruppenId)}
|
||||
${renderSubSection('Finisher', sortByName(g.finisher || []), 'finisher', renderFinisher, g.gruppenId)}
|
||||
${renderSubSection('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId)}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
@@ -1010,6 +1037,7 @@ function itemCard(id, kurzText, badges, rows, kind, gruppenId) {
|
||||
const actionBtns = `<div class="item-action-btns">
|
||||
<button class="btn-item-edit" onclick="openEditItemModal('${esc(id)}',event)">✎ Bearbeiten</button>
|
||||
<button class="btn-item-edit" onclick="duplicateItem('${kind}','${esc(id)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
|
||||
<button class="btn-item-edit" onclick="openMoveModal('${kind}','${esc(id)}','${esc(gruppenId)}',event)">↪ Verschieben</button>
|
||||
<button class="btn-item-delete" onclick="deleteItem('${kind}','${esc(id)}','${esc(gruppenId)}',event)">✕ Löschen</button>
|
||||
</div>`;
|
||||
return `<div class="item" id="item-${esc(id)}">
|
||||
@@ -1073,6 +1101,52 @@ async function duplicateItem(kind, itemId, gruppenId, event) {
|
||||
else document.getElementById('gruppeActionError').textContent = 'Fehler beim Duplizieren (HTTP ' + r.status + ').';
|
||||
}
|
||||
|
||||
let _moveState = null;
|
||||
function openMoveModal(kind, itemId, currentGruppeId, event) {
|
||||
event.stopPropagation();
|
||||
const item = _itemData[itemId]; if (!item) return;
|
||||
_moveState = { kind, itemId, currentGruppeId };
|
||||
document.getElementById('moveModalItemName').textContent = item.kurzText || itemId;
|
||||
document.getElementById('moveModalError').textContent = '';
|
||||
const sel = document.getElementById('moveModalSelect');
|
||||
sel.innerHTML = '';
|
||||
Object.values(_gruppeData)
|
||||
.filter(g => g.gruppenId !== currentGruppeId)
|
||||
.sort((a, b) => (a.name || '').localeCompare(b.name || '', 'de'))
|
||||
.forEach(g => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = g.gruppenId;
|
||||
opt.textContent = g.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
document.getElementById('moveModal').classList.add('open');
|
||||
}
|
||||
function closeMoveModal() {
|
||||
document.getElementById('moveModal').classList.remove('open');
|
||||
_moveState = null;
|
||||
}
|
||||
document.getElementById('moveModalCancel').addEventListener('click', closeMoveModal);
|
||||
document.getElementById('moveModal').addEventListener('click', e => {
|
||||
if (e.target === document.getElementById('moveModal')) closeMoveModal();
|
||||
});
|
||||
document.getElementById('moveModalOk').addEventListener('click', async () => {
|
||||
if (!_moveState) return;
|
||||
const { kind, itemId, currentGruppeId } = _moveState;
|
||||
const targetGruppeId = document.getElementById('moveModalSelect').value;
|
||||
if (!targetGruppeId) return;
|
||||
const r = await fetch(`/admin/aufgabengruppen/items/${kind}/${itemId}/move?targetGruppeId=${targetGruppeId}`, {
|
||||
method: 'PUT'
|
||||
});
|
||||
if (r.ok || r.status === 204) {
|
||||
closeMoveModal();
|
||||
openItemId = null;
|
||||
pendingExpandId = currentGruppeId;
|
||||
loadAdminGruppen();
|
||||
} else {
|
||||
document.getElementById('moveModalError').textContent = 'Fehler beim Verschieben (HTTP ' + r.status + ').';
|
||||
}
|
||||
});
|
||||
|
||||
function sortByLevelThenName(items) {
|
||||
return items.slice().sort((a, b) => {
|
||||
const la = a.level ?? 999, lb = b.level ?? 999;
|
||||
@@ -1081,6 +1155,14 @@ function sortByLevelThenName(items) {
|
||||
});
|
||||
}
|
||||
function sortByName(items) { return items.slice().sort((a, b) => (a.kurzText || '').localeCompare(b.kurzText || '', 'de')); }
|
||||
const GESCHLECHT_ORDER = { WEIBLICH: 0, DIVERS: 1, MAENNLICH: 2 };
|
||||
function sortByGeschlecht(items) {
|
||||
return items.slice().sort((a, b) => {
|
||||
const ga = GESCHLECHT_ORDER[a.geschlecht] ?? 99, gb = GESCHLECHT_ORDER[b.geschlecht] ?? 99;
|
||||
if (ga !== gb) return ga - gb;
|
||||
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
|
||||
});
|
||||
}
|
||||
function formatSek(von, bis) {
|
||||
if (von != null && bis != null) return `${von}–${bis} s`;
|
||||
if (von != null) return `ab ${von} s`; if (bis != null) return `bis ${bis} s`; return '';
|
||||
@@ -1308,8 +1390,116 @@ function openEditItemModal(itemId, event) {
|
||||
itemModal.classList.add('open'); document.getElementById('iKurzText').focus();
|
||||
}
|
||||
|
||||
function closeItemModal() { itemModal.classList.remove('open'); closeToySearch(); document.getElementById('iPlaceholderHint').style.display = 'none'; }
|
||||
function closeItemModal() { itemModal.classList.remove('open'); closeToySearch(); document.getElementById('iPlaceholderHint').style.display = 'none'; document.getElementById('iTextAC').style.display = 'none'; }
|
||||
function togglePlaceholderHint() { const el = document.getElementById('iPlaceholderHint'); el.style.display = el.style.display === 'none' ? 'block' : 'none'; }
|
||||
|
||||
(function() {
|
||||
const STATIC = ['{AKTIV}', '{PASSIV}'];
|
||||
const ta = document.getElementById('iText');
|
||||
const ac = document.getElementById('iTextAC');
|
||||
let _allToys = [];
|
||||
let _items = [];
|
||||
let _wordStart = 0;
|
||||
let activeIdx = -1;
|
||||
|
||||
function currentWord() {
|
||||
const pos = ta.selectionStart;
|
||||
let start = pos;
|
||||
while (start > 0 && !/\s/.test(ta.value[start - 1])) start--;
|
||||
return { word: ta.value.slice(start, pos).toLowerCase(), start };
|
||||
}
|
||||
function buildItems(filter) {
|
||||
const f = filter || '';
|
||||
_items = STATIC.filter(s => !f || s.toLowerCase().includes(f)).map(s => ({ label: s, insert: s }));
|
||||
const toys = _allToys.filter(t => !f || t.name.toLowerCase().includes(f));
|
||||
if (toys.length) {
|
||||
_items.push({ separator: true, label: 'Toys' });
|
||||
toys.forEach(t => _items.push({ label: t.name, insert: t.name, toyId: t.toyId }));
|
||||
}
|
||||
}
|
||||
function selectables() { return _items.map((it, i) => it.separator ? null : i).filter(i => i !== null); }
|
||||
function renderItems() {
|
||||
ac.innerHTML = '';
|
||||
activeIdx = -1;
|
||||
_items.forEach((item, i) => {
|
||||
if (item.separator) {
|
||||
const sep = document.createElement('div');
|
||||
sep.className = 'ac-separator';
|
||||
sep.textContent = item.label;
|
||||
ac.appendChild(sep);
|
||||
} else {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'ac-item';
|
||||
div.dataset.idx = String(i);
|
||||
div.textContent = item.label;
|
||||
div.addEventListener('mousedown', e => { e.preventDefault(); doInsert(item); });
|
||||
div.addEventListener('mouseover', () => setActive(i));
|
||||
ac.appendChild(div);
|
||||
}
|
||||
});
|
||||
const s = selectables();
|
||||
if (s.length) { setActive(s[0]); ac.style.display = 'block'; } else hideAC();
|
||||
}
|
||||
function showAC(toys) {
|
||||
_allToys = toys || [];
|
||||
const { word, start } = currentWord();
|
||||
_wordStart = start;
|
||||
buildItems(word);
|
||||
const rect = ta.getBoundingClientRect();
|
||||
ac.style.left = rect.left + 'px';
|
||||
ac.style.top = (rect.bottom + 4) + 'px';
|
||||
renderItems();
|
||||
}
|
||||
function hideAC() { ac.style.display = 'none'; activeIdx = -1; }
|
||||
function setActive(i) {
|
||||
activeIdx = i;
|
||||
let activeEl = null;
|
||||
ac.querySelectorAll('.ac-item').forEach(el => {
|
||||
const on = parseInt(el.dataset.idx) === i;
|
||||
el.classList.toggle('ac-item-active', on);
|
||||
if (on) activeEl = el;
|
||||
});
|
||||
if (activeEl) activeEl.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
function doInsert(item) {
|
||||
const end = ta.selectionStart;
|
||||
ta.value = ta.value.slice(0, _wordStart) + item.insert + ta.value.slice(end);
|
||||
ta.selectionStart = ta.selectionEnd = _wordStart + item.insert.length;
|
||||
ta.focus();
|
||||
if (item.toyId) {
|
||||
const already = (_selectedToys || []).find(t => t.toyId === item.toyId);
|
||||
if (!already) toggleToyFromSearch(item.toyId);
|
||||
}
|
||||
hideAC();
|
||||
}
|
||||
ta.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey && e.code === 'Space') {
|
||||
e.preventDefault();
|
||||
_loadAvailableToys().then(toys => showAC(toys)).catch(() => showAC([]));
|
||||
return;
|
||||
}
|
||||
if (ac.style.display !== 'block') return;
|
||||
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); hideAC(); return; }
|
||||
const s = selectables();
|
||||
if (!s.length) return;
|
||||
const pos = s.indexOf(activeIdx);
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setActive(s[(pos + 1) % s.length]); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); setActive(s[(pos - 1 + s.length) % s.length]); }
|
||||
else if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const item = _items[activeIdx];
|
||||
if (item && !item.separator) doInsert(item);
|
||||
}
|
||||
});
|
||||
ta.addEventListener('input', () => {
|
||||
if (ac.style.display !== 'block') return;
|
||||
const { word, start } = currentWord();
|
||||
_wordStart = start;
|
||||
buildItems(word);
|
||||
renderItems();
|
||||
});
|
||||
document.addEventListener('mousedown', e => { if (!ac.contains(e.target) && e.target !== ta) hideAC(); });
|
||||
})();
|
||||
document.getElementById('itemCancelBtn').addEventListener('click', closeItemModal);
|
||||
itemModal.addEventListener('click', e => { if (e.target === itemModal) closeItemModal(); });
|
||||
|
||||
|
||||
@@ -883,7 +883,8 @@ body.app {
|
||||
}
|
||||
|
||||
/* Benachrichtigungen */
|
||||
.topbar-notif-item--unread { background: rgba(var(--color-primary-rgb, 231,57,84), 0.04); }
|
||||
.topbar-notif-item--unread { background: rgba(var(--color-primary-rgb, 231,57,84), 0.07); border-left: 3px solid var(--color-primary); }
|
||||
.topbar-notif-item--unread:hover { background: rgba(var(--color-primary-rgb, 231,57,84), 0.12); }
|
||||
.topbar-notif-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0;url=/games/bdsm/neubdsm.html">
|
||||
<title>BDSM Game – xXx Sphere</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>window.location.replace('/games/bdsm/neubdsm.html');</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0;url=/games/bdsm/neubdsm.html">
|
||||
<title>BDSM Game – xXx Sphere</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>window.location.replace('/games/bdsm/neubdsm.html');</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0;url=/games/bdsm/neubdsm.html">
|
||||
<title>BDSM Game – xXx Sphere</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>window.location.replace('/games/bdsm/neubdsm.html');</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0;url=/games/bdsm/neubdsm.html">
|
||||
<title>BDSM Game – xXx Sphere</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>window.location.replace('/games/bdsm/neubdsm.html');</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -104,7 +104,7 @@
|
||||
<div style="font-size:2.5rem;margin-bottom:1rem;">⚠️</div>
|
||||
<h2 style="margin-bottom:0.5rem;">Einladung nicht gefunden</h2>
|
||||
<p style="color:var(--color-muted);">Diese Einladung existiert nicht oder wurde bereits bearbeitet.</p>
|
||||
<a href="/community/einladungen.html" style="display:inline-block;margin-top:1.5rem;padding:0.65rem 1.5rem;background:var(--color-primary);color:#fff;border-radius:8px;text-decoration:none;font-weight:600;">Zu meinen Einladungen</a>
|
||||
<a href="/games/common/einladungen.html" style="display:inline-block;margin-top:1.5rem;padding:0.65rem 1.5rem;background:var(--color-primary);color:#fff;border-radius:8px;text-decoration:none;font-weight:600;">Zu meinen Einladungen</a>
|
||||
</div>
|
||||
|
||||
<div id="stateAlready" style="text-align:center;padding:3rem 1rem;">
|
||||
@@ -117,7 +117,7 @@
|
||||
<div style="font-size:2.5rem;margin-bottom:1rem;">✕</div>
|
||||
<h2 style="margin-bottom:0.5rem;">Einladung abgelehnt</h2>
|
||||
<p style="color:var(--color-muted);">Du hast die Einladung abgelehnt. Der Keyholder wurde benachrichtigt.</p>
|
||||
<a href="/community/einladungen.html" style="display:inline-block;margin-top:1.5rem;padding:0.65rem 1.5rem;background:var(--color-primary);color:#fff;border-radius:8px;text-decoration:none;font-weight:600;">Zu meinen Einladungen</a>
|
||||
<a href="/games/common/einladungen.html" style="display:inline-block;margin-top:1.5rem;padding:0.65rem 1.5rem;background:var(--color-primary);color:#fff;border-radius:8px;text-decoration:none;font-weight:600;">Zu meinen Einladungen</a>
|
||||
</div>
|
||||
|
||||
<div id="stateInvite" style="display:none;">
|
||||
|
||||
@@ -839,7 +839,7 @@
|
||||
|
||||
const data = await res.json();
|
||||
if (data.lockeeInvitationSent) {
|
||||
window.location.href = '/community/einladungen.html?tab=gesendet';
|
||||
window.location.href = '/games/common/einladungen.html?tab=gesendet';
|
||||
} else if (!data.unlockCode) {
|
||||
// Trust: kein Code, direkt weiter
|
||||
const isTimeLock = selectedTemplate && selectedTemplate._type === 'timelock';
|
||||
|
||||
@@ -227,6 +227,15 @@
|
||||
background: rgba(233,69,96,0.12); color: var(--color-primary);
|
||||
border-radius: 3px; padding: 0.05rem 0.3rem; font-size: 0.75rem;
|
||||
}
|
||||
#iTextAC { position: fixed; z-index: 9999; background: var(--color-surface, #1e1e2e);
|
||||
border: 1px solid var(--color-border, #444); border-radius: 6px;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,.5); display: none; overflow: hidden; min-width: 180px; max-height: 280px; overflow-y: auto; }
|
||||
.ac-item { padding: 0.45rem 0.9rem; cursor: pointer; font-size: 0.88rem;
|
||||
font-family: monospace; color: var(--color-text, #cdd6f4); user-select: none; }
|
||||
.ac-item:hover, .ac-item-active { background: var(--color-primary, #cba6f7); color: #1e1e2e; }
|
||||
.ac-separator { padding: 0.2rem 0.7rem; font-size: 0.72rem; color: var(--color-muted);
|
||||
text-transform: uppercase; letter-spacing: 0.05em; background: rgba(255,255,255,0.04);
|
||||
pointer-events: none; border-top: 1px solid rgba(136,136,136,0.2); margin-top: 2px; }
|
||||
|
||||
/* ── Item-Add-Modal extra ── */
|
||||
.modal-two-col { display: flex; gap: 0.75rem; }
|
||||
@@ -407,6 +416,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="iText" rows="4" maxlength="4000" placeholder="Ausführliche Beschreibung…"></textarea>
|
||||
<div id="iTextAC"></div>
|
||||
|
||||
<!-- Finisher: Geschlecht -->
|
||||
<div id="iGeschlechtRow">
|
||||
@@ -734,7 +744,7 @@
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
|
||||
${IS_BDSM_MODE ? renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), 'strafe', renderStrafe, g.gruppenId, type) : ''}
|
||||
${IS_BDSM_MODE ? renderSubSection('Zeitstrafen',sortByName(g.sperren || []), 'zeitstrafe',renderZeitstrafe, g.gruppenId, type) : ''}
|
||||
${renderSubSection('Finisher', sortByName(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
|
||||
${renderSubSection('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
@@ -945,6 +955,14 @@
|
||||
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
|
||||
});
|
||||
}
|
||||
const GESCHLECHT_ORDER = { WEIBLICH: 0, DIVERS: 1, MAENNLICH: 2 };
|
||||
function sortByGeschlecht(items) {
|
||||
return items.slice().sort((a, b) => {
|
||||
const ga = GESCHLECHT_ORDER[a.geschlecht] ?? 99, gb = GESCHLECHT_ORDER[b.geschlecht] ?? 99;
|
||||
if (ga !== gb) return ga - gb;
|
||||
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
|
||||
});
|
||||
}
|
||||
function sortByName(items) {
|
||||
return items.slice().sort((a, b) => (a.kurzText || '').localeCompare(b.kurzText || '', 'de'));
|
||||
}
|
||||
@@ -1429,12 +1447,120 @@
|
||||
document.getElementById('iKurzText').focus();
|
||||
}
|
||||
|
||||
function closeItemModal() { itemModal.classList.remove('open'); closeToySearch(); document.getElementById('iPlaceholderHint').style.display = 'none'; }
|
||||
function closeItemModal() { itemModal.classList.remove('open'); closeToySearch(); document.getElementById('iPlaceholderHint').style.display = 'none'; document.getElementById('iTextAC').style.display = 'none'; }
|
||||
function togglePlaceholderHint() {
|
||||
const el = document.getElementById('iPlaceholderHint');
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
(function() {
|
||||
const STATIC = ['{AKTIV}', '{PASSIV}'];
|
||||
const ta = document.getElementById('iText');
|
||||
const ac = document.getElementById('iTextAC');
|
||||
let _allToys = [];
|
||||
let _items = [];
|
||||
let _wordStart = 0;
|
||||
let activeIdx = -1;
|
||||
|
||||
function currentWord() {
|
||||
const pos = ta.selectionStart;
|
||||
let start = pos;
|
||||
while (start > 0 && !/\s/.test(ta.value[start - 1])) start--;
|
||||
return { word: ta.value.slice(start, pos).toLowerCase(), start };
|
||||
}
|
||||
function buildItems(filter) {
|
||||
const f = filter || '';
|
||||
_items = STATIC.filter(s => !f || s.toLowerCase().includes(f)).map(s => ({ label: s, insert: s }));
|
||||
const toys = _allToys.filter(t => !f || t.name.toLowerCase().includes(f));
|
||||
if (toys.length) {
|
||||
_items.push({ separator: true, label: 'Toys' });
|
||||
toys.forEach(t => _items.push({ label: t.name, insert: t.name, toyId: t.toyId }));
|
||||
}
|
||||
}
|
||||
function selectables() { return _items.map((it, i) => it.separator ? null : i).filter(i => i !== null); }
|
||||
function renderItems() {
|
||||
ac.innerHTML = '';
|
||||
activeIdx = -1;
|
||||
_items.forEach((item, i) => {
|
||||
if (item.separator) {
|
||||
const sep = document.createElement('div');
|
||||
sep.className = 'ac-separator';
|
||||
sep.textContent = item.label;
|
||||
ac.appendChild(sep);
|
||||
} else {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'ac-item';
|
||||
div.dataset.idx = String(i);
|
||||
div.textContent = item.label;
|
||||
div.addEventListener('mousedown', e => { e.preventDefault(); doInsert(item); });
|
||||
div.addEventListener('mouseover', () => setActive(i));
|
||||
ac.appendChild(div);
|
||||
}
|
||||
});
|
||||
const s = selectables();
|
||||
if (s.length) { setActive(s[0]); ac.style.display = 'block'; } else hideAC();
|
||||
}
|
||||
function showAC(toys) {
|
||||
_allToys = toys || [];
|
||||
const { word, start } = currentWord();
|
||||
_wordStart = start;
|
||||
buildItems(word);
|
||||
const rect = ta.getBoundingClientRect();
|
||||
ac.style.left = rect.left + 'px';
|
||||
ac.style.top = (rect.bottom + 4) + 'px';
|
||||
renderItems();
|
||||
}
|
||||
function hideAC() { ac.style.display = 'none'; activeIdx = -1; }
|
||||
function setActive(i) {
|
||||
activeIdx = i;
|
||||
let activeEl = null;
|
||||
ac.querySelectorAll('.ac-item').forEach(el => {
|
||||
const on = parseInt(el.dataset.idx) === i;
|
||||
el.classList.toggle('ac-item-active', on);
|
||||
if (on) activeEl = el;
|
||||
});
|
||||
if (activeEl) activeEl.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
function doInsert(item) {
|
||||
const end = ta.selectionStart;
|
||||
ta.value = ta.value.slice(0, _wordStart) + item.insert + ta.value.slice(end);
|
||||
ta.selectionStart = ta.selectionEnd = _wordStart + item.insert.length;
|
||||
ta.focus();
|
||||
if (item.toyId) {
|
||||
const already = (_selectedToys || []).find(t => t.toyId === item.toyId);
|
||||
if (!already) toggleToyFromSearch(item.toyId);
|
||||
}
|
||||
hideAC();
|
||||
}
|
||||
ta.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey && e.code === 'Space') {
|
||||
e.preventDefault();
|
||||
_loadAvailableToys().then(toys => showAC(toys)).catch(() => showAC([]));
|
||||
return;
|
||||
}
|
||||
if (ac.style.display !== 'block') return;
|
||||
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); hideAC(); return; }
|
||||
const s = selectables();
|
||||
if (!s.length) return;
|
||||
const pos = s.indexOf(activeIdx);
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setActive(s[(pos + 1) % s.length]); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); setActive(s[(pos - 1 + s.length) % s.length]); }
|
||||
else if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const item = _items[activeIdx];
|
||||
if (item && !item.separator) doInsert(item);
|
||||
}
|
||||
});
|
||||
ta.addEventListener('input', () => {
|
||||
if (ac.style.display !== 'block') return;
|
||||
const { word, start } = currentWord();
|
||||
_wordStart = start;
|
||||
buildItems(word);
|
||||
renderItems();
|
||||
});
|
||||
document.addEventListener('mousedown', e => { if (!ac.contains(e.target) && e.target !== ta) hideAC(); });
|
||||
})();
|
||||
|
||||
document.getElementById('itemCancelBtn').addEventListener('click', closeItemModal);
|
||||
itemModal.addEventListener('click', e => { if (e.target === itemModal) closeItemModal(); });
|
||||
|
||||
@@ -602,6 +602,18 @@
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmText').textContent = text;
|
||||
document.getElementById('confirmModal').classList.add('open');
|
||||
document.querySelector('#confirmModal .confirm-modal-cancel').style.display = '';
|
||||
return new Promise(resolve => {
|
||||
_confirmResolve = resolve;
|
||||
document.getElementById('confirmOkBtn').onclick = () => { confirmClose(true); };
|
||||
});
|
||||
}
|
||||
|
||||
function showInfo(title, text) {
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmText').textContent = text;
|
||||
document.getElementById('confirmModal').classList.add('open');
|
||||
document.querySelector('#confirmModal .confirm-modal-cancel').style.display = 'none';
|
||||
return new Promise(resolve => {
|
||||
_confirmResolve = resolve;
|
||||
document.getElementById('confirmOkBtn').onclick = () => { confirmClose(true); };
|
||||
@@ -885,6 +897,9 @@
|
||||
removeRecvItem(key);
|
||||
if (mode === 'OWN_DEVICE') {
|
||||
window.location.href = `/games/bdsm/neubdsm.html`;
|
||||
} else if (mode === 'HOST_DEVICE') {
|
||||
await showInfo('Einladung angenommen', 'Das Spiel findet am Gerät des Hosts statt. Du wirst zur Startseite weitergeleitet.');
|
||||
window.location.href = '/userhome.html';
|
||||
}
|
||||
} catch (_) {
|
||||
errEl.textContent = 'Fehler beim Speichern der Antwort.';
|
||||
@@ -933,6 +948,7 @@
|
||||
if (mode === 'OWN_DEVICE') {
|
||||
window.location.href = '/games/vanilla/neuvanilla.html';
|
||||
} else if (mode === 'HOST_DEVICE') {
|
||||
await showInfo('Einladung angenommen', 'Das Spiel findet am Gerät des Hosts statt. Du wirst zur Startseite weitergeleitet.');
|
||||
window.location.href = '/userhome.html';
|
||||
}
|
||||
} catch (_) {
|
||||
@@ -190,7 +190,7 @@
|
||||
<div class="acc-body" id="acc-aufgaben-body">
|
||||
<div id="guestAufgabenHint" class="guest-hint" style="display:none;">Aufgaben werden vom Host festgelegt – nur zur Ansicht.</div>
|
||||
<p style="font-size:0.85rem;color:var(--color-muted);margin-bottom:0.75rem;">
|
||||
Gruppen verwalten: <a href="/games/bdsm/aufgaben.html" style="color:var(--color-primary);">Aufgaben-Verwaltung (Vanilla)</a>
|
||||
Gruppen verwalten: <a href="/games/common/aufgaben.html" style="color:var(--color-primary);">Aufgaben-Verwaltung (Vanilla)</a>
|
||||
</p>
|
||||
<div id="sectionOwn">
|
||||
<div class="aufgaben-section-label"><label class="select-all-label"><input type="checkbox" class="select-all-cb" data-list="listOwn"> Eigene Gruppen</label></div>
|
||||
@@ -566,11 +566,14 @@
|
||||
const ul = document.getElementById(containerId);
|
||||
const section = ul.closest('[id^="section"]');
|
||||
const selectAllWrap = section?.querySelector('.select-all-label');
|
||||
if (!gruppen.length) {
|
||||
const filtered = gruppen.filter(g =>
|
||||
(g.aufgaben || []).length > 0 || (g.finisher || []).length > 0
|
||||
);
|
||||
if (!filtered.length) {
|
||||
ul.innerHTML = '<li class="empty-hint">Keine Gruppen vorhanden.</li>';
|
||||
if (selectAllWrap) selectAllWrap.style.visibility = 'hidden'; return;
|
||||
}
|
||||
ul.innerHTML = gruppen.map(g => {
|
||||
ul.innerHTML = filtered.map(g => {
|
||||
const checked = savedGruppen.has(g.gruppenId);
|
||||
return `<li><label class="gruppe-item${checked ? ' is-checked' : ''}">
|
||||
<input type="checkbox" value="${g.gruppenId}"${checked ? ' checked' : ''}>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0;url=/games/vanilla/neuvanilla.html">
|
||||
<title>Vanilla Game – xXx Sphere</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>window.location.replace('/games/vanilla/neuvanilla.html');</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,8 +10,8 @@
|
||||
{ href: '/games/vanilla/neuvanilla.html', icon: I('PLAY_NEW'), label: 'Neue Session', id: 'navVanillaNeu' },
|
||||
{ href: '#', icon: I('WAITING'), label: 'Aktive Session', id: 'navVanillaAktiv' },
|
||||
{ href: '/games/vanilla/vanillaingame.html', icon: I('PLAY_ACTIVE'), label: 'Im Spiel', id: 'navVanillaImSpiel' },
|
||||
{ href: '/games/bdsm/aufgaben.html', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/chastity/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/common/aufgaben.html', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/common/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/chastity/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||
]
|
||||
},
|
||||
@@ -22,8 +22,8 @@
|
||||
{ href: '/games/bdsm/neubdsm.html', icon: I('PLAY_NEW'), label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '#', icon: I('WAITING'), label: 'Aktive Session', id: 'navBdsmAktiv' },
|
||||
{ href: '/games/bdsm/bdsmingame.html', icon: I('PLAY_ACTIVE'), label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/games/bdsm/aufgaben.html?mode=bdsm', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/chastity/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/common/aufgaben.html?mode=bdsm', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/games/common/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/games/chastity/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||
]
|
||||
},
|
||||
@@ -56,7 +56,7 @@
|
||||
{ href: '/community/nachrichten.html', icon: I('MESSAGES'), label: 'Nachrichten', badgeId: null },
|
||||
{ href: '/community/benachrichtigungen.html', icon: I('NOTIFICATIONS'), label: 'Benachrichtigungen', badgeId: null },
|
||||
{ href: '/community/gruppen.html', icon: I('GROUPS'), label: 'Gruppen', badgeId: 'socialGruppenBadge'},
|
||||
{ href: '/community/einladungen.html', icon: I('INVITATIONS'), label: 'Einladungen', badgeId: null },
|
||||
{ href: '/games/common/einladungen.html', icon: I('INVITATIONS'), label: 'Einladungen', badgeId: null },
|
||||
];
|
||||
const socialNav = socialLinks.map(({ href, icon, label, badgeId }) => {
|
||||
const cls = path === href ? ' class="active"' : '';
|
||||
|
||||
@@ -85,6 +85,12 @@
|
||||
} catch(ex) {}
|
||||
});
|
||||
|
||||
es.addEventListener('INVITATION', () => {
|
||||
try {
|
||||
if (typeof window.__topbarReloadInvBadge === 'function') window.__topbarReloadInvBadge();
|
||||
} catch(ex) {}
|
||||
});
|
||||
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
// Vor dem Reconnect prüfen ob noch eingeloggt (verhindert Endlos-Schleife bei abgelaufener Session)
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
||||
</div>
|
||||
<div class="topbar-panel-body" id="topbarInvBody"></div>
|
||||
<div class="topbar-panel-footer"><a href="/community/einladungen.html">Alle anzeigen →</a></div>
|
||||
<div class="topbar-panel-footer"><a href="/games/common/einladungen.html">Alle anzeigen →</a></div>
|
||||
</div>
|
||||
|
||||
<div class="topbar-panel" id="topbarProfilePanel">
|
||||
@@ -275,17 +275,17 @@
|
||||
try {
|
||||
const res = await fetch('/notifications');
|
||||
if (!res.ok) { body.innerHTML = '<div class="topbar-panel-hint">Keine Benachrichtigungen.</div>'; return; }
|
||||
const notifs = await res.json();
|
||||
if (!notifs.length) { body.innerHTML = '<div class="topbar-panel-hint">Keine neuen Benachrichtigungen.</div>'; return; }
|
||||
const unread = (await res.json()).filter(n => !n.read);
|
||||
if (!unread.length) { body.innerHTML = '<div class="topbar-panel-hint">Keine neuen Benachrichtigungen.</div>'; return; }
|
||||
body.innerHTML = '';
|
||||
notifs.forEach(n => {
|
||||
unread.forEach(n => {
|
||||
const el = document.createElement('div');
|
||||
const tag = n.targetUrl ? 'a' : 'div';
|
||||
const href = n.targetUrl ? `href="${esc(n.targetUrl)}"` : '';
|
||||
const av = n.senderAvatar
|
||||
? `<img src="data:image/png;base64,${esc(n.senderAvatar)}" class="topbar-item-avatar" alt="">`
|
||||
: `<span class="topbar-item-avatar topbar-item-avatar--placeholder">${IC('PROFILE')}</span>`;
|
||||
el.innerHTML = `<${tag} ${href} class="topbar-panel-item topbar-notif-item">
|
||||
el.innerHTML = `<${tag} ${href} class="topbar-panel-item topbar-notif-item${n.read ? '' : ' topbar-notif-item--unread'}">
|
||||
${av}
|
||||
<div class="topbar-panel-item-body">
|
||||
<div style="font-size:0.85rem;line-height:1.4;">${esc(n.text)}</div>
|
||||
@@ -323,18 +323,21 @@
|
||||
if (!body) return;
|
||||
body.innerHTML = '<div class="topbar-panel-hint">Wird geladen…</div>';
|
||||
try {
|
||||
const [lr, kr, br] = await Promise.all([
|
||||
const [lr, kr, br, vr] = await Promise.all([
|
||||
fetch('/lockee/invitations/mine'),
|
||||
fetch('/keyholder/invitations/mine'),
|
||||
fetch('/bdsm/einladung/pending')
|
||||
fetch('/bdsm/einladung/pending'),
|
||||
fetch('/vanilla/einladung/pending')
|
||||
]);
|
||||
const lockee = lr.ok ? await lr.json() : [];
|
||||
const kh = kr.ok ? await kr.json() : [];
|
||||
const bdsm = br.ok ? await br.json() : [];
|
||||
const lockee = lr.ok ? await lr.json() : [];
|
||||
const kh = kr.ok ? await kr.json() : [];
|
||||
const bdsm = br.ok ? await br.json() : [];
|
||||
const vanilla = vr.ok ? await vr.json() : [];
|
||||
const all = [
|
||||
...lockee.map(i => ({ ...i, _type: 'lockee' })),
|
||||
...kh.map(i => ({ ...i, _type: 'keyholder' })),
|
||||
...bdsm.map(i => ({ ...i, _type: 'bdsm' }))
|
||||
...bdsm.map(i => ({ ...i, _type: 'bdsm' })),
|
||||
...vanilla.map(i => ({ ...i, _type: 'vanilla' }))
|
||||
];
|
||||
if (!all.length) { body.innerHTML = '<div class="topbar-panel-hint">Keine offenen Einladungen.</div>'; return; }
|
||||
body.innerHTML = '';
|
||||
@@ -343,66 +346,35 @@
|
||||
}
|
||||
|
||||
function buildInvCard(inv) {
|
||||
let typeIcon, typeName, line, declineUrl, declineMethod = 'DELETE', declineBody = null, acceptHtml;
|
||||
let typeIcon, typeName, line;
|
||||
|
||||
if (inv._type === 'lockee') {
|
||||
typeIcon = IC('LOCK'); typeName = 'Lockee-Einladung'; line = inv.lockName || 'Lock';
|
||||
declineUrl = '/lockee/invitation/' + encodeURIComponent(inv.token);
|
||||
acceptHtml = `<a href="/community/einladungen.html" class="topbar-inv-btn topbar-inv-btn--accept">Details</a>`;
|
||||
} else if (inv._type === 'keyholder') {
|
||||
typeIcon = IC('KEY'); typeName = 'Keyholder-Einladung'; line = inv.lockName || 'Lock';
|
||||
declineUrl = '/keyholder/invitations/mine/' + encodeURIComponent(inv.token);
|
||||
acceptHtml = `<a href="/keyholder/invitation/${esc(inv.token)}" class="topbar-inv-btn topbar-inv-btn--accept">Annehmen</a>`;
|
||||
} else if (inv._type === 'vanilla') {
|
||||
typeIcon = IC('INVITATIONS'); typeName = 'Vanilla Game'; line = inv.inviterName || 'Einladung';
|
||||
} else {
|
||||
typeIcon = IC('BDSM'); typeName = 'BDSM Game'; line = inv.senderName || 'Einladung';
|
||||
const id = inv.id || inv.einladungId || '';
|
||||
declineUrl = '/bdsm/einladung/' + encodeURIComponent(id) + '/antwort';
|
||||
declineMethod = 'PUT';
|
||||
declineBody = JSON.stringify({ annahme: false });
|
||||
acceptHtml = `<a href="/games/bdsm/bdsm-einladung.html?id=${esc(id)}" class="topbar-inv-btn topbar-inv-btn--accept">Details</a>`;
|
||||
}
|
||||
|
||||
const senderPic = inv.senderAvatar || inv.lockOwnerAvatar;
|
||||
const senderPic = inv.senderAvatar || inv.lockOwnerAvatar || inv.inviterAvatar;
|
||||
const av = senderPic
|
||||
? `<img src="data:image/png;base64,${esc(senderPic)}" class="topbar-item-avatar" alt="">`
|
||||
: `<span class="topbar-item-avatar topbar-item-avatar--placeholder">${IC('PROFILE')}</span>`;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'topbar-panel-item topbar-inv-card';
|
||||
div.style.cursor = 'pointer';
|
||||
div.innerHTML = `${av}
|
||||
<div class="topbar-panel-item-body">
|
||||
<div class="topbar-panel-item-sub">${typeIcon} ${typeName}</div>
|
||||
<div style="font-weight:600;font-size:0.88rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(line)}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.3rem;flex-shrink:0;">
|
||||
<button class="topbar-inv-btn topbar-inv-btn--decline"
|
||||
data-url="${esc(declineUrl)}"
|
||||
data-method="${declineMethod}"
|
||||
data-body="${esc(declineBody || '')}"
|
||||
onclick="window.__topbarDecline(this)">✕</button>
|
||||
${acceptHtml}
|
||||
</div>`;
|
||||
div.addEventListener('click', () => { window.location.href = '/games/common/einladungen.html'; });
|
||||
return div;
|
||||
}
|
||||
|
||||
window.__topbarDecline = async function (btn) {
|
||||
btn.disabled = true;
|
||||
const url = btn.dataset.url;
|
||||
const method = btn.dataset.method;
|
||||
const body = btn.dataset.body || null;
|
||||
try {
|
||||
const opts = { method };
|
||||
if (body) { opts.headers = { 'Content-Type': 'application/json' }; opts.body = body; }
|
||||
const res = await fetch(url, opts);
|
||||
if (res.ok || res.status === 204) {
|
||||
const card = btn.closest('.topbar-inv-card');
|
||||
if (card) card.remove();
|
||||
const remaining = document.getElementById('topbarInvBody')?.querySelectorAll('.topbar-inv-card').length || 0;
|
||||
setTopbarBadge('inv', remaining);
|
||||
} else { btn.disabled = false; }
|
||||
} catch (e) { btn.disabled = false; }
|
||||
};
|
||||
|
||||
// ── Badge-Verwaltung ──
|
||||
function setTopbarBadge(type, count) {
|
||||
const map = { msg: 'topbarMsgBadge', notif: 'topbarNotifBadge', inv: 'topbarInvBadge' };
|
||||
@@ -415,9 +387,7 @@
|
||||
// Für social-sidebar.js zugänglich
|
||||
window.__topbarSetBadge = setTopbarBadge;
|
||||
|
||||
function loadInitialBadges() {
|
||||
fetch('/social/messages/unread/count').then(r => r.ok ? r.json() : 0).then(n => setTopbarBadge('msg', n)).catch(() => {});
|
||||
fetch('/notifications/unread/count').then(r => r.ok ? r.json() : 0).then(n => setTopbarBadge('notif', n)).catch(() => {});
|
||||
function reloadInvBadge() {
|
||||
Promise.all([
|
||||
fetch('/lockee/invitations/mine/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/keyholder/invitations/mine/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
@@ -425,4 +395,11 @@
|
||||
fetch('/vanilla/einladung/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0)
|
||||
]).then(([l, k, b, v]) => setTopbarBadge('inv', l + k + b + v)).catch(() => {});
|
||||
}
|
||||
window.__topbarReloadInvBadge = reloadInvBadge;
|
||||
|
||||
function loadInitialBadges() {
|
||||
fetch('/social/messages/unread/count').then(r => r.ok ? r.json() : 0).then(n => setTopbarBadge('msg', n)).catch(() => {});
|
||||
fetch('/notifications/unread/count').then(r => r.ok ? r.json() : 0).then(n => setTopbarBadge('notif', n)).catch(() => {});
|
||||
reloadInvBadge();
|
||||
}
|
||||
})();
|
||||
|
||||