Änderungen am Msessage System, Datenschutz-Einstellungen hinzugefügt, BDSM und CardLock Game weiterverfeinert

This commit is contained in:
2026-03-18 22:35:21 +01:00
parent d22b87a79b
commit 655cdad796
106 changed files with 7156 additions and 3686 deletions

View File

@@ -23,7 +23,9 @@
"Bash(./gradlew compileJava -q 2>&1 | tail -30)", "Bash(./gradlew compileJava -q 2>&1 | tail -30)",
"Bash(ls -lah /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/*.java)", "Bash(ls -lah /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/*.java)",
"Bash(for f:*)", "Bash(for f:*)",
"Bash(ls:*)" "Bash(ls:*)",
"Bash(./gradlew compileJava)",
"Bash(./gradlew build:*)"
] ]
} }
} }

View File

@@ -1,5 +1,5 @@
#Tue Mar 17 19:55:50 CET 2026 #Wed Mar 18 15:28:58 CET 2026
display=\:0 display=\:0
host=Mario-Linux host=Mario-Linux
process-id=148721 process-id=26624
user=mario user=mario

View File

@@ -1097,3 +1097,973 @@ Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106) at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204) at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204)
... 68 more ... 68 more
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-17 23:01:40.053
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
!ENTRY org.eclipse.jdt.core 4 4 2026-03-17 23:01:40.298
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!SESSION 2026-03-18 15:28:52.788 -----------------------------------------------
eclipse.buildId=4.39.0.20260305-0817
java.version=21.0.10
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 -product org.eclipse.epp.package.java.product
!ENTRY ch.qos.logback.classic 1 0 2026-03-18 15:28:53.799
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
!ENTRY ch.qos.logback.classic 1 0 2026-03-18 15:28:58.414
!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-18 15:28:58.572
!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-18 15:28:58.572
!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-18 15:28:58.702
!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-18 15:28:58.702
!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.workbench 4 0 2026-03-18 16:50:37.017
!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception
!STACK 0
org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden))
at org.eclipse.swt.SWT.error(SWT.java:4950)
at org.eclipse.swt.SWT.error(SWT.java:4865)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207)
at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198)
at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38)
at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76)
at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83)
at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57)
at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194)
at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58)
at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183)
at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136)
at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950)
at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34)
at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65)
at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1)
at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151)
at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132)
at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73)
at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48)
at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55)
at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61)
at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424)
at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365)
at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111)
at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89)
at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467)
at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493)
at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297)
at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91)
at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661)
at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290)
at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102)
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051)
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153)
at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583)
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173)
at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185)
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204)
... 68 more
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 17:25:44.906
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.ui.workbench 4 0 2026-03-18 17:33:38.648
!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception
!STACK 0
org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden))
at org.eclipse.swt.SWT.error(SWT.java:4950)
at org.eclipse.swt.SWT.error(SWT.java:4865)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207)
at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198)
at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38)
at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76)
at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83)
at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57)
at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194)
at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58)
at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183)
at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136)
at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950)
at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34)
at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65)
at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1)
at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151)
at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132)
at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73)
at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48)
at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55)
at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61)
at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424)
at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365)
at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111)
at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89)
at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467)
at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493)
at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297)
at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91)
at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661)
at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290)
at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102)
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051)
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153)
at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583)
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173)
at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185)
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204)
... 68 more
!ENTRY org.eclipse.ui.workbench 4 0 2026-03-18 17:35:19.625
!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception
!STACK 0
org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden))
at org.eclipse.swt.SWT.error(SWT.java:4950)
at org.eclipse.swt.SWT.error(SWT.java:4865)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207)
at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198)
at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38)
at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76)
at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83)
at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57)
at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194)
at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58)
at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183)
at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136)
at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950)
at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34)
at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65)
at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1)
at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151)
at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132)
at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73)
at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48)
at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55)
at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61)
at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424)
at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365)
at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111)
at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89)
at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467)
at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493)
at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297)
at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91)
at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661)
at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290)
at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102)
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051)
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153)
at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583)
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173)
at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185)
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204)
... 68 more
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 18:02:17.714
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jface 2 0 2026-03-18 18:11:03.312
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-18 18:11:03.312
!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)
!ENTRY org.eclipse.lsp4e 2 0 2026-03-18 18:12:46.286
!MESSAGE Javadoc unavailable. Failed to obtain it.
!STACK 0
java.lang.InterruptedException
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 18:12:57.074
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 18:13:45.585
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 18:25:16.508
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 18:46:49.386
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 18:48:21.541
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 18:48:31.516
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 18:49:49.718
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 18:57:46.512
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 19:16:53.710
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 19:30:00.005
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.ui.workbench 4 0 2026-03-18 19:30:35.497
!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception
!STACK 0
org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden))
at org.eclipse.swt.SWT.error(SWT.java:4950)
at org.eclipse.swt.SWT.error(SWT.java:4865)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207)
at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198)
at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38)
at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76)
at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83)
at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57)
at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194)
at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58)
at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183)
at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136)
at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950)
at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34)
at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65)
at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1)
at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151)
at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132)
at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73)
at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48)
at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55)
at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61)
at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424)
at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365)
at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111)
at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89)
at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467)
at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493)
at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297)
at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91)
at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661)
at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290)
at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102)
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051)
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153)
at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583)
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173)
at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185)
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204)
... 68 more
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 19:32:18.384
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 19:32:45.429
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.ui.workbench 4 0 2026-03-18 19:33:19.167
!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception
!STACK 0
org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden))
at org.eclipse.swt.SWT.error(SWT.java:4950)
at org.eclipse.swt.SWT.error(SWT.java:4865)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207)
at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198)
at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38)
at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76)
at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83)
at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57)
at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194)
at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58)
at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183)
at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136)
at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950)
at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34)
at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65)
at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1)
at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151)
at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132)
at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73)
at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48)
at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55)
at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61)
at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424)
at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365)
at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111)
at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89)
at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467)
at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493)
at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297)
at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91)
at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661)
at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290)
at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102)
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051)
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153)
at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583)
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173)
at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185)
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204)
... 68 more
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 19:34:29.072
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 19:39:46.763
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 19:46:32.144
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 19:50:39.778
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 19:57:10.404
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 20:08:07.916
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 20:08:34.316
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 20:50:39.209
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 21:02:50.177
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 21:02:51.971
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 22:02:40.121
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 22:21:25.529
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 22:34:27.436
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-18 22:34:35.489
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
!ENTRY org.eclipse.jdt.core 4 4 2026-03-18 22:34:35.761
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,105 +1,106 @@
INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core
2628068441.index
2403041570.index
1704193220.index
3051047092.index
2466743981.index
2852275968.index
3371017197.index
2376429633.index
2236377038.index
833027591.index
4080887926.index
3424266581.index
2488355463.index
3514351073.index
1090991043.index
13999064.index
352173590.index
2799433906.index
3718169413.index
2157310342.index
1730373086.index
341080888.index
3839581777.index
3572667491.index
2826242951.index
2874180664.index
134995224.index
2455962971.index
1063231598.index
3769604005.index
794464160.index 794464160.index
1067882983.index 2655170954.index
3547251881.index 176453541.index
1022297761.index 677104696.index
341080888.index
774576701.index
4134502745.index 4134502745.index
1780956574.index 41199409.index
369020172.index 134995224.index
2217896880.index 2217896880.index
4025319337.index
900586112.index
2929476459.index
2065500052.index
3051047092.index
815902026.index
3718169413.index
721517855.index
369020172.index
3899935016.index
2157310342.index
2488355463.index
3572667491.index
2799433906.index
675283020.index
2032345814.index
3839581777.index
2466743981.index
13999064.index
673436610.index
3972616808.index
1914043487.index
3154281632.index
1117161889.index
983587063.index
766461225.index
286641703.index
3371017197.index
4080887926.index
2941512597.index
1730373086.index
3882180612.index
4020783879.index
2900482015.index
3059431983.index
833027591.index
13156219.index
4088356365.index
37241354.index
1295630681.index
2701419231.index
3939420913.index
1067882983.index
1318022262.index
773718761.index
2311226047.index
3539841425.index
1865797976.index
2455962971.index
836138551.index
2389383899.index
2226615777.index
3515611559.index
3728851734.index
2826242951.index
2899155238.index
3763224039.index
2138052223.index
2236377038.index
3547251881.index
371677185.index 371677185.index
2127778675.index 2127778675.index
2389383899.index 2519831052.index
2701419231.index 1063231598.index
2874180664.index
2939623059.index
2576972120.index
2376429633.index
2628068441.index
1090991043.index
1138623861.index
1223891870.index
3769604005.index
3158780236.index
2237645717.index
2852275968.index
2403041570.index
1704193220.index
2004806901.index
3952767374.index
3416862923.index 3416862923.index
3912907421.index 3912907421.index
1256436118.index
815902026.index
900586112.index
766461225.index
1117161889.index
675283020.index
4088356365.index
836138551.index
2226615777.index
3539841425.index
2939623059.index
3728851734.index
3972616808.index
2494834982.index
1938594271.index
4025319337.index
781064456.index 781064456.index
2032345814.index 352173590.index
2655170954.index
983587063.index
3939420913.index
2247053514.index
1138623861.index
3882180612.index
2237645717.index
721517855.index
176453541.index
4020783879.index
3899935016.index
2576972120.index
1223891870.index
3158780236.index
677104696.index
766439048.index 766439048.index
41199409.index 3424266581.index
2900482015.index 2247053514.index
3952767374.index 1765772496.index
773718761.index 3514351073.index
2519831052.index
286641703.index
3515611559.index
1865797976.index
3059431983.index
2929476459.index
774576701.index
13156219.index
2311226047.index
2138052223.index
3763224039.index
3154281632.index
1318022262.index
2065500052.index
37241354.index
2899155238.index
673436610.index
1914043487.index
1295630681.index
2941512597.index
3892622621.index 3892622621.index
2004806901.index 2494834982.index
1780956574.index
1022297761.index
1938594271.index
1256436118.index

View File

@@ -18,4 +18,5 @@
<fullyQualifiedTypeName name="lombok.Getter"/> <fullyQualifiedTypeName name="lombok.Getter"/>
<fullyQualifiedTypeName name="lombok.Setter"/> <fullyQualifiedTypeName name="lombok.Setter"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.Test"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.Test"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.KeyholderCardLock"/>
</qualifiedTypeNameHistroy> </qualifiedTypeNameHistroy>

View File

@@ -24,6 +24,10 @@
<section name="RenameInformationPopup"> <section name="RenameInformationPopup">
</section> </section>
<section name="org.eclipse.ltk.ui.refactoring.settings"> <section name="org.eclipse.ltk.ui.refactoring.settings">
<item key="renameSubpackages" value="false"/>
<item key="updateTextualMatches" value="false"/>
<item key="updateQualifiedNames" value="false"/>
<item key="patterns" value="*"/>
</section> </section>
<section name="org.eclipse.jdt.internal.ui.dialogs.OpenTypeSelectionDialog2"> <section name="org.eclipse.jdt.internal.ui.dialogs.OpenTypeSelectionDialog2">
<item key="ShowStatusLine" value="true"/> <item key="ShowStatusLine" value="true"/>
@@ -68,4 +72,13 @@
<section name="BuildPathsPropertyPage"> <section name="BuildPathsPropertyPage">
<item key="pageIndex" value="3"/> <item key="pageIndex" value="3"/>
</section> </section>
<section name="org.eclipse.jdt.internal.ui.typehierarchy.QuickHierarchy">
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_WIDTH" value="400"/>
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_HEIGHT" value="346"/>
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_USE_PERSISTED_SIZE" value="true"/>
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_USE_PERSISTED_LOCATION" value="false"/>
</section>
<section name="NewPackageWizardPage">
<item key="create_package_info_java" value="false"/>
</section>
</section> </section>

View File

@@ -21,3 +21,4 @@
2026-03-17 19:39:55,931 [Worker-1: 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-17 19:39:55,931 [Worker-1: 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-17 19:49:51,508 [Worker-2: 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-17 19:49:51,508 [Worker-2: 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-17 19:55:53,050 [Worker-1: 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-17 19:55:53,050 [Worker-1: 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-18 15:29:01,371 [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.

View File

@@ -12,7 +12,7 @@
<item key="DIALOG_X_ORIGIN" value="20"/> <item key="DIALOG_X_ORIGIN" value="20"/>
<item key="DIALOG_Y_ORIGIN" value="20"/> <item key="DIALOG_Y_ORIGIN" value="20"/>
<item key="DIALOG_WIDTH" value="874"/> <item key="DIALOG_WIDTH" value="874"/>
<item key="DIALOG_HEIGHT" value="995"/> <item key="DIALOG_HEIGHT" value="1050"/>
<item key="DIALOG_FONT_NAME" value="1|Ubuntu Sans|11.0|0|GTK|1|"/> <item key="DIALOG_FONT_NAME" value="1|Ubuntu Sans|11.0|0|GTK|1|"/>
</section> </section>
<section name="ImportExportAction"> <section name="ImportExportAction">

View File

@@ -1,3 +1,3 @@
#Tue Mar 17 19:55:50 CET 2026 #Wed Mar 18 15:28:58 CET 2026
org.eclipse.core.runtime=2 org.eclipse.core.runtime=2
org.eclipse.platform=4.39.0.v20260226-0420 org.eclipse.platform=4.39.0.v20260226-0420

View File

@@ -1,6 +1,6 @@
package de.oaa.xxx.aufgaben; package de.oaa.xxx.aufgaben;
import de.oaa.xxx.session.GeschlechtEnum; import de.oaa.xxx.games.bdsm.GeschlechtEnum;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;

View File

@@ -67,9 +67,9 @@ public class AufgabeEntity {
public Aufgabe toAufgabe() { public Aufgabe toAufgabe() {
Aufgabe aufgabe = new Aufgabe(); Aufgabe aufgabe = new Aufgabe();
aufgabe.setAufgabeId(aufgabeId); aufgabe.setAufgabeId(aufgabeId);
aufgabe.setBenoetigtAktiv(benoetigtAktiv); aufgabe.setBenoetigtAktiv(benoetigtAktiv != null ? new ArrayList<>(benoetigtAktiv) : new ArrayList<>());
aufgabe.setBenoetigteToys(benoetigteToys.stream().map(ToyEntity::toToy).toList()); aufgabe.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>());
aufgabe.setBenoetigtPassiv(benoetigtPassiv); aufgabe.setBenoetigtPassiv(benoetigtPassiv != null ? new ArrayList<>(benoetigtPassiv) : new ArrayList<>());
aufgabe.setGruppeId(aufgabenGruppe.getGruppenId()); aufgabe.setGruppeId(aufgabenGruppe.getGruppenId());
aufgabe.setKurzText(kurzText); aufgabe.setKurzText(kurzText);
aufgabe.setLevel(level); aufgabe.setLevel(level);

View File

@@ -18,7 +18,7 @@ import java.util.UUID;
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "aufgabenGruppe") @Table(name = "aufgaben_gruppe")
public class AufgabenGruppeEntity { public class AufgabenGruppeEntity {
@Id @Id

View File

@@ -2,7 +2,7 @@ package de.oaa.xxx.aufgaben.entity;
import de.oaa.xxx.aufgaben.Finisher; import de.oaa.xxx.aufgaben.Finisher;
import de.oaa.xxx.aufgaben.Werkzeug; import de.oaa.xxx.aufgaben.Werkzeug;
import de.oaa.xxx.session.GeschlechtEnum; import de.oaa.xxx.games.bdsm.GeschlechtEnum;
import jakarta.persistence.CascadeType; import jakarta.persistence.CascadeType;
import jakarta.persistence.CollectionTable; import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column; import jakarta.persistence.Column;
@@ -67,9 +67,9 @@ public class FinisherEntity {
finisher.setKurzText(kurzText); finisher.setKurzText(kurzText);
finisher.setText(text); finisher.setText(text);
finisher.setGeschlecht(geschlecht); finisher.setGeschlecht(geschlecht);
finisher.setBenoetigtAktiv(benoetigtAktiv); finisher.setBenoetigtAktiv(benoetigtAktiv != null ? new ArrayList<>(benoetigtAktiv) : new ArrayList<>());
finisher.setBenoetigtPassiv(benoetigtPassiv); finisher.setBenoetigtPassiv(benoetigtPassiv != null ? new ArrayList<>(benoetigtPassiv) : new ArrayList<>());
finisher.setBenoetigteToys(benoetigteToys.stream().map(ToyEntity::toToy).toList()); finisher.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>());
finisher.setGruppeId(aufgabenGruppe.getGruppenId()); finisher.setGruppeId(aufgabenGruppe.getGruppenId());
return finisher; return finisher;
} }

View File

@@ -67,7 +67,7 @@ public class SperreEntity {
sperre.setMinutenBis(minutenBis); sperre.setMinutenBis(minutenBis);
sperre.setMinutenVon(minutenVon); sperre.setMinutenVon(minutenVon);
sperre.setReleaseText(releaseText); sperre.setReleaseText(releaseText);
sperre.setSperreFuer(sperreFuer); sperre.setSperreFuer(sperreFuer != null ? new ArrayList<>(sperreFuer) : new ArrayList<>());
sperre.setText(text); sperre.setText(text);
sperre.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>()); sperre.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>());
return sperre; return sperre;

View File

@@ -67,9 +67,9 @@ public class StrafeEntity {
public Strafe toStrafe() { public Strafe toStrafe() {
Strafe strafe = new Strafe(); Strafe strafe = new Strafe();
strafe.setStrafeId(strafeId); strafe.setStrafeId(strafeId);
strafe.setBenoetigtAktiv(benoetigtAktiv); strafe.setBenoetigtAktiv(benoetigtAktiv != null ? new ArrayList<>(benoetigtAktiv) : new ArrayList<>());
strafe.setBenoetigteToys(benoetigteToys.stream().map(ToyEntity::toToy).toList()); strafe.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>());
strafe.setBenoetigtPassiv(benoetigtPassiv); strafe.setBenoetigtPassiv(benoetigtPassiv != null ? new ArrayList<>(benoetigtPassiv) : new ArrayList<>());
strafe.setGruppeId(aufgabenGruppe.getGruppenId()); strafe.setGruppeId(aufgabenGruppe.getGruppenId());
strafe.setKurzText(kurzText); strafe.setKurzText(kurzText);
strafe.setLevel(level); strafe.setLevel(level);

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session; package de.oaa.xxx.games.bdsm;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;

View File

@@ -1,21 +1,25 @@
package de.oaa.xxx.session; package de.oaa.xxx.games.bdsm;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@Getter import java.util.UUID;
@Setter
public class AufgabeAnzeige { @Getter
@Setter
private String nameAktiverMitspieler; public class AufgabeAnzeige {
private String aufgabeText;
private Integer timer; private String nameAktiverMitspieler;
private Callback callback; private String aufgabeText;
private Integer level; private Integer timer;
private Callback callback;
@Override private Integer level;
public String toString() { private UUID mitspielerId;
return "AufgabeAnzeige[mitspieler=" + nameAktiverMitspieler + ", level=" + level + ", timer=" + timer private boolean eigenesGeraet;
+ ", callback=" + (callback != null ? callback.getClass().getSimpleName() : null) + "]";
} @Override
} public String toString() {
return "AufgabeAnzeige[mitspieler=" + nameAktiverMitspieler + ", level=" + level + ", timer=" + timer
+ ", callback=" + (callback != null ? callback.getClass().getSimpleName() : null) + "]";
}
}

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session; package de.oaa.xxx.games.bdsm;
public enum AufgabeArt { public enum AufgabeArt {
AUFGABE, AUFGABE,

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session; package de.oaa.xxx.games.bdsm;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -8,10 +8,11 @@ import java.util.UUID;
@Getter @Getter
@Setter @Setter
public class Session { public class BdsmGame {
private UUID sessionId; private UUID sessionId;
private UUID userId; private UUID userId;
private UUID setupId;
private Integer wahrscheinlichkeitSperre; private Integer wahrscheinlichkeitSperre;
private Integer wahrscheinlichkeitStrafe; private Integer wahrscheinlichkeitStrafe;
private Integer aufgabenProLevel; private Integer aufgabenProLevel;

View File

@@ -1,251 +1,265 @@
package de.oaa.xxx.session; package de.oaa.xxx.games.bdsm;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.session.aufgaben.Aufgabe; import de.oaa.xxx.games.bdsm.aufgaben.Aufgabe;
import de.oaa.xxx.session.aufgaben.AufgabenList; import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
import de.oaa.xxx.session.aufgaben.Sperre; import de.oaa.xxx.games.bdsm.aufgaben.Sperre;
import de.oaa.xxx.session.aufgaben.Strafe; import de.oaa.xxx.games.bdsm.aufgaben.Strafe;
import de.oaa.xxx.session.entity.SessionEntity; import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.session.sperre.SperreCallback; import de.oaa.xxx.games.bdsm.sperre.SperreCallback;
import de.oaa.xxx.session.sperre.SperrenVerlaengernCallback; import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback;
public class SessionDurchfuehren { public class BdsmGameDurchfuehren {
private final AufgabenList aufgabenList; private final AufgabenList aufgabenList;
private final List<Mitspieler> mitspieler = new ArrayList<>(); private final List<Mitspieler> mitspieler = new ArrayList<>();
private final List<AktiveSperre> aktiveSperren = new ArrayList<>(); private final List<AktiveSperre> aktiveSperren = new ArrayList<>();
private final Integer wahrscheinlichkeitSperre; private final Integer wahrscheinlichkeitSperre;
private final Integer wahrscheinlichkeitStrafe; private final Integer wahrscheinlichkeitStrafe;
private int aufgabenProLevel; private int aufgabenProLevel;
private int level; private int level;
private int aufgabenAufAktuellemLevel; private int aufgabenAufAktuellemLevel;
public SessionDurchfuehren(SessionEntity entity) throws Exception { public BdsmGameDurchfuehren(BdsmGameEntity entity) throws Exception {
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
aufgabenList = objectMapper.readValue(entity.getAufgaben(), AufgabenList.class); aufgabenList = objectMapper.readValue(entity.getAufgaben(), AufgabenList.class);
entity.getMitspieler().forEach(mitspielerEntity -> mitspieler.add(mitspielerEntity.toMitspieler())); entity.getMitspieler().forEach(mitspielerEntity -> mitspieler.add(mitspielerEntity.toMitspieler()));
entity.getAktiveSperren().forEach(sperreEntity -> aktiveSperren.add(sperreEntity.toSperre(mitspieler))); entity.getAktiveSperren().forEach(sperreEntity -> aktiveSperren.add(sperreEntity.toSperre(mitspieler)));
wahrscheinlichkeitSperre = entity.getWahrscheinlichkeitSperre(); wahrscheinlichkeitSperre = entity.getWahrscheinlichkeitSperre();
wahrscheinlichkeitStrafe = entity.getWahrscheinlichkeitStrafe(); wahrscheinlichkeitStrafe = entity.getWahrscheinlichkeitStrafe();
this.aufgabenProLevel = entity.getAufgabenProLevel() != null ? entity.getAufgabenProLevel() : 5; this.aufgabenProLevel = entity.getAufgabenProLevel() != null ? entity.getAufgabenProLevel() : 5;
this.level = entity.getLevel() != null ? entity.getLevel() : 1; this.level = entity.getLevel() != null ? entity.getLevel() : 1;
this.aufgabenAufAktuellemLevel = entity.getAufgabenAufAktuellemLevel() != null ? entity.getAufgabenAufAktuellemLevel() : 0; this.aufgabenAufAktuellemLevel = entity.getAufgabenAufAktuellemLevel() != null ? entity.getAufgabenAufAktuellemLevel() : 0;
} }
public AufgabeAnzeige getNext() { public AufgabeAnzeige getNext() {
checkLevel(); checkLevel();
if (level == 6) { if (level == 6) {
return null; return null;
} }
AufgabeAnzeige anzeige = null; AufgabeAnzeige anzeige = null;
int nextInt = new Random().nextInt(1, 100); int nextInt = new Random().nextInt(1, 100);
if (nextInt == 1) { if (nextInt == 1) {
anzeige = findUltimativeStrafe(); anzeige = findUltimativeStrafe();
} else if (nextInt == 2) { } else if (nextInt == 2) {
anzeige = findSperreVerlaengern(); anzeige = findSperreVerlaengern();
} else if (nextInt > wahrscheinlichkeitSperre + wahrscheinlichkeitStrafe + 2) { } else if (nextInt > wahrscheinlichkeitSperre + wahrscheinlichkeitStrafe + 2) {
anzeige = findeAufgabe(); anzeige = findeAufgabe();
} else if (nextInt > wahrscheinlichkeitSperre + 2) { } else if (nextInt > wahrscheinlichkeitSperre + 2) {
anzeige = findeStrafe(); anzeige = findeStrafe();
} else { } else {
anzeige = findeSperre(); anzeige = findeSperre();
} }
if (anzeige == null) { if (anzeige == null) {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV); Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv); Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
String text = "Ups, da ist etwas schief gelaufen. Keine potenzielle Aufgabe gefunden. Entweder seid ihr inzwischen so gut weggesperrt, dass wirklich keine Aufgaben mehr zur Verfügung stehen, oder uns ist ein Fehler unterlaufen. {AKTIV} und {PASSIV} überbrücken die Zeit mit ein wenig Petting."; String text = "Ups, da ist etwas schief gelaufen. Keine potenzielle Aufgabe gefunden. Entweder seid ihr inzwischen so gut weggesperrt, dass wirklich keine Aufgaben mehr zur Verfügung stehen, oder uns ist ein Fehler unterlaufen. {AKTIV} und {PASSIV} überbrücken die Zeit mit ein wenig Petting.";
anzeige = new AufgabeAnzeige(); anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv != null ? aktiv.getName() : ""); anzeige.setNameAktiverMitspieler(aktiv != null ? aktiv.getName() : "");
anzeige.setAufgabeText(getAnzeigeText(text, aktiv != null ? aktiv.getName() : "?", passiv != null ? passiv.getName() : "?")); setMitspielerInfo(anzeige, aktiv);
anzeige.setTimer(120); anzeige.setAufgabeText(getAnzeigeText(text, aktiv != null ? aktiv.getName() : "?", passiv != null ? passiv.getName() : "?"));
} anzeige.setTimer(120);
return anzeige; }
} return anzeige;
}
public void backToLvl5() {
this.level = 5; public void backToLvl5() {
this.aufgabenAufAktuellemLevel = 0; this.level = 5;
} this.aufgabenAufAktuellemLevel = 0;
}
public List<AufgabeAnzeige> getFinisher() {
var list = new ArrayList<AufgabeAnzeige>(); public List<AufgabeAnzeige> getFinisher() {
List.of(GeschlechtEnum.WEIBLICH, GeschlechtEnum.DIVERS, GeschlechtEnum.MAENNLICH).forEach(geschlecht -> { var list = new ArrayList<AufgabeAnzeige>();
mitspieler.stream().filter(m -> geschlecht == m.getGeschlecht()).toList().forEach(cumming -> { List.of(GeschlechtEnum.WEIBLICH, GeschlechtEnum.DIVERS, GeschlechtEnum.MAENNLICH).forEach(geschlecht -> {
var partner = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, cumming); mitspieler.stream().filter(m -> geschlecht == m.getGeschlecht()).toList().forEach(cumming -> {
var finishers = aufgabenList.getFinisher().stream() var partner = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, cumming);
.filter(finisher -> geschlecht == finisher.getGeschlecht()) var finishers = aufgabenList.getFinisher().stream()
.toList(); .filter(finisher -> geschlecht == finisher.getGeschlecht())
if (!finishers.isEmpty()) { .toList();
var aufgabe = finishers.get(new Random().nextInt(list.size())); if (!finishers.isEmpty()) {
AufgabeAnzeige anzeige = new AufgabeAnzeige(); var aufgabe = finishers.get(new Random().nextInt(list.size()));
anzeige.setNameAktiverMitspieler(cumming.getName()); AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(), anzeige.setNameAktiverMitspieler(cumming.getName());
cumming.getName(), partner != null ? partner.getName() : "")); setMitspielerInfo(anzeige, cumming);
list.add(anzeige); anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(),
} cumming.getName(), partner != null ? partner.getName() : ""));
}); list.add(anzeige);
}); }
return list; });
} });
return list;
private void checkLevel() { }
if (++aufgabenAufAktuellemLevel >= 1 + aufgabenProLevel) {
aufgabenAufAktuellemLevel = 0; private void checkLevel() {
level++; if (++aufgabenAufAktuellemLevel >= 1 + aufgabenProLevel) {
} aufgabenAufAktuellemLevel = 0;
} level++;
}
private AufgabeAnzeige findUltimativeStrafe() { }
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) { private void setMitspielerInfo(AufgabeAnzeige anzeige, Mitspieler aktiv) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv); if (aktiv != null) {
if (passiv != null) { anzeige.setMitspielerId(aktiv.getId());
String text = "{AKTIV}, verschnüre {PASSIV} fachmännisch inkl. KG, Plugs, Knebel, Augenbinde und was dir sonst einfällt. Nutze die Ruhe für was auch immer du möchtest."; anzeige.setEigenesGeraet(aktiv.isEigenesGeraet());
AufgabeAnzeige anzeige = new AufgabeAnzeige(); }
anzeige.setNameAktiverMitspieler(aktiv.getName()); }
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setTimer(new Random().nextInt(1800, 7200)); private AufgabeAnzeige findUltimativeStrafe() {
return anzeige; Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
} if (aktiv != null) {
} Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
return findeStrafe(); if (passiv != null) {
} String text = "{AKTIV}, verschnüre {PASSIV} fachmännisch inkl. KG, Plugs, Knebel, Augenbinde und was dir sonst einfällt. Nutze die Ruhe für was auch immer du möchtest.";
AufgabeAnzeige anzeige = new AufgabeAnzeige();
private AufgabeAnzeige findSperreVerlaengern() { anzeige.setNameAktiverMitspieler(aktiv.getName());
if (!aktiveSperren.isEmpty()) { setMitspielerInfo(anzeige, aktiv);
AktiveSperre sperre = aktiveSperren.get(new Random().nextInt(aktiveSperren.size())); anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
Mitspieler passiv = sperre.getMitspieler(); anzeige.setTimer(new Random().nextInt(1800, 7200));
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV, passiv); return anzeige;
if (aktiv != null) { }
String text = "{AKTIV}, du entscheidest. Sollen alle bestehenden Zeitstrafen von {PASSIV} verlängert werden...?"; }
AufgabeAnzeige anzeige = new AufgabeAnzeige(); return findeStrafe();
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName())); }
anzeige.setNameAktiverMitspieler(aktiv.getName());
SperrenVerlaengernCallback callback = new SperrenVerlaengernCallback(); private AufgabeAnzeige findSperreVerlaengern() {
callback.setFaktor(new Random().nextInt(2, 4)); if (!aktiveSperren.isEmpty()) {
callback.setSpielerId(passiv.getId()); AktiveSperre sperre = aktiveSperren.get(new Random().nextInt(aktiveSperren.size()));
anzeige.setCallback(callback); Mitspieler passiv = sperre.getMitspieler();
return anzeige; Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV, passiv);
} if (aktiv != null) {
} String text = "{AKTIV}, du entscheidest. Sollen alle bestehenden Zeitstrafen von {PASSIV} verlängert werden...?";
return findeSperre(); AufgabeAnzeige anzeige = new AufgabeAnzeige();
} anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setNameAktiverMitspieler(aktiv.getName());
private AufgabeAnzeige findeAufgabe() { setMitspielerInfo(anzeige, aktiv);
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV); SperrenVerlaengernCallback callback = new SperrenVerlaengernCallback();
if (aktiv != null) { callback.setFaktor(new Random().nextInt(2, 4));
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv); callback.setSpielerId(passiv.getId());
if (passiv != null) { anzeige.setCallback(callback);
List<Aufgabe> list = aufgabenList.getAufgaben().stream() return anzeige;
.filter(aufgabe -> aufgabe.isAufgabePassend(level, aktiv, passiv)) }
.collect(Collectors.toList()); }
if (!list.isEmpty()) { return findeSperre();
Aufgabe aufgabe = list.get(new Random().nextInt(list.size())); }
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName()); private AufgabeAnzeige findeAufgabe() {
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(), aktiv.getName(), passiv.getName())); Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
if (aufgabe.getSekundenVon() != null) { if (aktiv != null) {
if (aufgabe.getSekundenBis() != null) { Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
anzeige.setTimer(new Random().nextInt(aufgabe.getSekundenVon(), aufgabe.getSekundenBis())); if (passiv != null) {
} else { List<Aufgabe> list = aufgabenList.getAufgaben().stream()
anzeige.setTimer(aufgabe.getSekundenVon()); .filter(aufgabe -> aufgabe.isAufgabePassend(level, aktiv, passiv))
} .collect(Collectors.toList());
} if (!list.isEmpty()) {
return anzeige; Aufgabe aufgabe = list.get(new Random().nextInt(list.size()));
} AufgabeAnzeige anzeige = new AufgabeAnzeige();
} anzeige.setNameAktiverMitspieler(aktiv.getName());
} setMitspielerInfo(anzeige, aktiv);
return findeStrafe(); anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(), aktiv.getName(), passiv.getName()));
} if (aufgabe.getSekundenVon() != null) {
if (aufgabe.getSekundenBis() != null) {
private AufgabeAnzeige findeStrafe() { anzeige.setTimer(new Random().nextInt(aufgabe.getSekundenVon(), aufgabe.getSekundenBis()));
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV); } else {
if (aktiv != null) { anzeige.setTimer(aufgabe.getSekundenVon());
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv); }
if (passiv != null) { }
List<Strafe> list = aufgabenList.getStrafen().stream() return anzeige;
.filter(strafe -> strafe.isAufgabePassend(level, aktiv, passiv)) }
.collect(Collectors.toList()); }
if (!list.isEmpty()) { }
Strafe strafe = list.get(new Random().nextInt(list.size())); return findeStrafe();
AufgabeAnzeige anzeige = new AufgabeAnzeige(); }
anzeige.setNameAktiverMitspieler(aktiv.getName());
anzeige.setAufgabeText(getAnzeigeText(strafe.getText(), aktiv.getName(), passiv.getName())); private AufgabeAnzeige findeStrafe() {
if (strafe.getSekundenVon() != null) { Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (strafe.getSekundenBis() != null) { if (aktiv != null) {
anzeige.setTimer(new Random().nextInt(strafe.getSekundenVon(), strafe.getSekundenBis())); Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
} else { if (passiv != null) {
anzeige.setTimer(strafe.getSekundenVon()); List<Strafe> list = aufgabenList.getStrafen().stream()
} .filter(strafe -> strafe.isAufgabePassend(level, aktiv, passiv))
} .collect(Collectors.toList());
return anzeige; if (!list.isEmpty()) {
} Strafe strafe = list.get(new Random().nextInt(list.size()));
} AufgabeAnzeige anzeige = new AufgabeAnzeige();
} anzeige.setNameAktiverMitspieler(aktiv.getName());
return findeSperre(); setMitspielerInfo(anzeige, aktiv);
} anzeige.setAufgabeText(getAnzeigeText(strafe.getText(), aktiv.getName(), passiv.getName()));
if (strafe.getSekundenVon() != null) {
private AufgabeAnzeige findeSperre() { if (strafe.getSekundenBis() != null) {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV); anzeige.setTimer(new Random().nextInt(strafe.getSekundenVon(), strafe.getSekundenBis()));
if (aktiv != null) { } else {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv); anzeige.setTimer(strafe.getSekundenVon());
if (passiv != null) { }
List<Sperre> list = aufgabenList.getSperren().stream() }
.filter(sperre -> sperre.isAufgabePassend(passiv)) return anzeige;
.collect(Collectors.toList()); }
if (!list.isEmpty()) { }
Sperre sperre = list.get(new Random().nextInt(list.size())); }
AufgabeAnzeige anzeige = new AufgabeAnzeige(); return findeSperre();
anzeige.setNameAktiverMitspieler(aktiv.getName()); }
anzeige.setAufgabeText(getAnzeigeText(sperre.getText(), aktiv.getName(), passiv.getName()));
SperreCallback callback = new SperreCallback(); private AufgabeAnzeige findeSperre() {
callback.setSperreId(sperre.getSperreId()); Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
callback.setSpielerId(passiv.getId()); if (aktiv != null) {
callback.setReleaseText(getAnzeigeText(sperre.getReleaseText(), aktiv.getName(), passiv.getName())); Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
anzeige.setCallback(callback); if (passiv != null) {
return anzeige; List<Sperre> list = aufgabenList.getSperren().stream()
} .filter(sperre -> sperre.isAufgabePassend(passiv))
} .collect(Collectors.toList());
} if (!list.isEmpty()) {
return null; Sperre sperre = list.get(new Random().nextInt(list.size()));
} AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
private String getAnzeigeText(String textMitPlatzhaltern, String nameAktiv, String namePassiv) { setMitspielerInfo(anzeige, aktiv);
return textMitPlatzhaltern.replace("{AKTIV}", nameAktiv).replace("{PASSIV}", namePassiv); anzeige.setAufgabeText(getAnzeigeText(sperre.getText(), aktiv.getName(), passiv.getName()));
} SperreCallback callback = new SperreCallback();
callback.setSperreId(sperre.getSperreId());
private Mitspieler findeMitspielerMitRolle(RolleEnum rolle) { callback.setSpielerId(passiv.getId());
List<Mitspieler> list = mitspieler.stream() callback.setReleaseText(getAnzeigeText(sperre.getReleaseText(), aktiv.getName(), passiv.getName()));
.filter(m -> m.getRollen().contains(rolle)) anzeige.setCallback(callback);
.toList(); return anzeige;
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size())); }
} }
}
private Mitspieler findeMitspielerMitRolle(RolleEnum rolle, Mitspieler gegenspieler) { return null;
if (gegenspieler == null) return findeMitspielerMitRolle(rolle); }
List<Mitspieler> list = mitspieler.stream()
.filter(m -> m != gegenspieler) private String getAnzeigeText(String textMitPlatzhaltern, String nameAktiv, String namePassiv) {
.filter(m -> m.isPassenderSpielpartner(gegenspieler)) return textMitPlatzhaltern.replace("{AKTIV}", nameAktiv).replace("{PASSIV}", namePassiv);
.filter(m -> m.getRollen().contains(rolle)) }
.toList();
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size())); private Mitspieler findeMitspielerMitRolle(RolleEnum rolle) {
} List<Mitspieler> list = mitspieler.stream()
.filter(m -> m.getRollen().contains(rolle))
public int getAufgabenAufAktuellemLevel() { .toList();
return aufgabenAufAktuellemLevel; return list.isEmpty() ? null : list.get(new Random().nextInt(list.size()));
} }
public int getLevel() { private Mitspieler findeMitspielerMitRolle(RolleEnum rolle, Mitspieler gegenspieler) {
return level; if (gegenspieler == null) return findeMitspielerMitRolle(rolle);
} List<Mitspieler> list = mitspieler.stream()
} .filter(m -> m != gegenspieler)
.filter(m -> m.isPassenderSpielpartner(gegenspieler))
.filter(m -> m.getRollen().contains(rolle))
.toList();
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size()));
}
public int getAufgabenAufAktuellemLevel() {
return aufgabenAufAktuellemLevel;
}
public int getLevel() {
return level;
}
}

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session; package de.oaa.xxx.games.bdsm;
import java.util.UUID; import java.util.UUID;

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session; package de.oaa.xxx.games.bdsm;
public enum GeschlechtEnum { public enum GeschlechtEnum {
WEIBLICH, WEIBLICH,

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session; package de.oaa.xxx.games.bdsm;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -11,6 +11,8 @@ import java.util.UUID;
public class Mitspieler { public class Mitspieler {
private UUID id; private UUID id;
private UUID userId;
private boolean eigenesGeraet;
private String name; private String name;
private GeschlechtEnum geschlecht; private GeschlechtEnum geschlecht;
private List<GeschlechtEnum> spieltMit; private List<GeschlechtEnum> spieltMit;

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session; package de.oaa.xxx.games.bdsm;
public enum RolleEnum { public enum RolleEnum {
BESTRAFUNG_AKTIV, BESTRAFUNG_AKTIV,

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session; package de.oaa.xxx.games.bdsm;
public enum Werkzeug { public enum Werkzeug {

View File

@@ -1,7 +1,7 @@
package de.oaa.xxx.session.aufgaben; package de.oaa.xxx.games.bdsm.aufgaben;
import de.oaa.xxx.session.Mitspieler; import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.session.Werkzeug; import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session.aufgaben; package de.oaa.xxx.games.bdsm.aufgaben;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;

View File

@@ -1,7 +1,7 @@
package de.oaa.xxx.session.aufgaben; package de.oaa.xxx.games.bdsm.aufgaben;
import de.oaa.xxx.session.GeschlechtEnum; import de.oaa.xxx.games.bdsm.GeschlechtEnum;
import de.oaa.xxx.session.Werkzeug; import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;

View File

@@ -1,7 +1,7 @@
package de.oaa.xxx.session.aufgaben; package de.oaa.xxx.games.bdsm.aufgaben;
import de.oaa.xxx.session.Mitspieler; import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.session.Werkzeug; import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;

View File

@@ -1,7 +1,7 @@
package de.oaa.xxx.session.aufgaben; package de.oaa.xxx.games.bdsm.aufgaben;
import de.oaa.xxx.session.Mitspieler; import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.session.Werkzeug; import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;

View File

@@ -0,0 +1,195 @@
package de.oaa.xxx.games.bdsm.controller;
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 org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/bdsm/einladung")
@Transactional
public class BdsmEinladungController {
private final BdsmEinladungRepository einladungRepository;
private final UserRepository userRepository;
private final FriendshipRepository friendshipRepository;
private final SystemMessageService systemMessageService;
public BdsmEinladungController(BdsmEinladungRepository einladungRepository,
UserRepository userRepository,
FriendshipRepository friendshipRepository,
SystemMessageService systemMessageService) {
this.einladungRepository = einladungRepository;
this.userRepository = userRepository;
this.friendshipRepository = friendshipRepository;
this.systemMessageService = systemMessageService;
}
record EinladungRequest(UUID setupId, int slotIndex, UUID inviteeId) {}
record AntwortRequest(boolean accepted, String mode) {} // mode: OWN_DEVICE | HOST_DEVICE
private UUID currentUserId(Principal principal) {
return userRepository.findByEmail(principal.getName())
.map(u -> u.getUserId()).orElse(null);
}
@PostMapping
public ResponseEntity<Map<String, Object>> sendEinladung(@RequestBody EinladungRequest req, Principal principal) {
UUID inviterId = currentUserId(principal);
if (inviterId == null) return ResponseEntity.status(401).build();
// Freundschaft prüfen
var friendship = friendshipRepository.findExisting(inviterId, req.inviteeId());
if (friendship.isEmpty() || friendship.get().getStatus() != de.oaa.xxx.social.entity.FriendshipEntity.Status.ACCEPTED) {
return ResponseEntity.status(403).build();
}
// Alte Einladung für diesen Slot canceln
einladungRepository.findBySetupId(req.setupId()).stream()
.filter(e -> e.getSlotIndex() == req.slotIndex() && e.getStatus() == Status.PENDING)
.forEach(e -> e.setStatus(Status.CANCELLED));
BdsmEinladungEntity entity = new BdsmEinladungEntity();
entity.setEinladungId(UUID.randomUUID());
entity.setSetupId(req.setupId());
entity.setInviterId(inviterId);
entity.setInviteeId(req.inviteeId());
entity.setSlotIndex(req.slotIndex());
entity.setStatus(Status.PENDING);
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.",
"/einladungen.html",
MessageCause.INVITATION
);
Map<String, Object> result = new LinkedHashMap<>();
result.put("einladungId", entity.getEinladungId());
return ResponseEntity.ok(result);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> cancelEinladung(@PathVariable UUID id, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);
if (e == null) return ResponseEntity.notFound().build();
if (!e.getInviterId().equals(userId)) return ResponseEntity.status(403).build();
e.setStatus(Status.CANCELLED);
return ResponseEntity.accepted().build();
}
@GetMapping
public ResponseEntity<List<Map<String, Object>>> getBySetupId(@RequestParam UUID setupId, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
List<Map<String, Object>> list = einladungRepository.findBySetupId(setupId).stream()
.map(this::toMap).toList();
return ResponseEntity.ok(list);
}
@GetMapping("/pending")
public ResponseEntity<List<Map<String, Object>>> getPending(Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
List<Map<String, Object>> list = einladungRepository.findByInviteeIdAndStatus(userId, Status.PENDING)
.stream().map(e -> {
Map<String, Object> m = toMap(e);
userRepository.findById(e.getInviterId()).ifPresent(u -> {
m.put("inviterName", u.getName());
m.put("inviterAvatar", u.getProfilePicture() != null ? u.getProfilePicture() : "");
});
return m;
}).toList();
return ResponseEntity.ok(list);
}
@GetMapping("/sent")
public ResponseEntity<List<Map<String, Object>>> getSent(Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
List<Map<String, Object>> list = einladungRepository.findByInviterIdAndStatus(userId, Status.PENDING)
.stream().map(e -> {
Map<String, Object> m = toMap(e);
userRepository.findById(e.getInviteeId()).ifPresent(u -> {
m.put("inviteeName", u.getName());
m.put("inviteeAvatar", u.getProfilePicture() != null ? u.getProfilePicture() : "");
});
return m;
}).toList();
return ResponseEntity.ok(list);
}
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> getById(@PathVariable UUID id, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);
if (e == null) return ResponseEntity.notFound().build();
if (!e.getInviteeId().equals(userId) && !e.getInviterId().equals(userId)) {
return ResponseEntity.status(403).build();
}
Map<String, Object> m = toMap(e);
userRepository.findById(e.getInviterId()).ifPresent(u -> {
m.put("inviterName", u.getName());
m.put("inviterAvatar", u.getProfilePicture() != null ? u.getProfilePicture() : "");
});
return ResponseEntity.ok(m);
}
@PutMapping("/{id}/antwort")
public ResponseEntity<Void> antwort(@PathVariable UUID id, @RequestBody AntwortRequest req, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);
if (e == null) return ResponseEntity.notFound().build();
if (!e.getInviteeId().equals(userId)) return ResponseEntity.status(403).build();
if (e.getStatus() != Status.PENDING) return ResponseEntity.badRequest().build();
if (!req.accepted()) {
e.setStatus(Status.DECLINED);
} else if ("OWN_DEVICE".equals(req.mode())) {
e.setStatus(Status.ACCEPTED_OWN);
} else {
e.setStatus(Status.ACCEPTED_HOST);
}
return ResponseEntity.accepted().build();
}
private Map<String, Object> toMap(BdsmEinladungEntity e) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("einladungId", e.getEinladungId());
m.put("setupId", e.getSetupId());
m.put("slotIndex", e.getSlotIndex());
m.put("inviteeId", e.getInviteeId());
m.put("inviterId", e.getInviterId());
m.put("status", e.getStatus().name());
m.put("sessionId", e.getSessionId());
return m;
}
}

View File

@@ -1,16 +1,22 @@
package de.oaa.xxx.session.controller; package de.oaa.xxx.games.bdsm.controller;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.session.AufgabeAnzeige; import de.oaa.xxx.games.bdsm.AufgabeAnzeige;
import de.oaa.xxx.session.Mitspieler; import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.session.Session; import de.oaa.xxx.games.bdsm.BdsmGame;
import de.oaa.xxx.session.SessionDurchfuehren; import de.oaa.xxx.games.bdsm.BdsmGameDurchfuehren;
import de.oaa.xxx.session.aufgaben.AufgabenList; import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
import de.oaa.xxx.session.entity.MitspielerEntity; import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import de.oaa.xxx.session.entity.SessionEntity; import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository; import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
import de.oaa.xxx.session.repository.MitspielerRepository; import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.session.repository.SessionRepository; import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import de.oaa.xxx.games.history.GameHistoryEntity;
import de.oaa.xxx.games.history.GameHistoryRepository;
import de.oaa.xxx.games.history.GameRole;
import de.oaa.xxx.games.history.GameType;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -21,59 +27,69 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.security.Principal;
import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@RequestMapping("/session") @RequestMapping("/bdsm")
@Transactional @Transactional
public class SessionController { public class BdsmGameController {
private static final Logger LOGGER = LoggerFactory.getLogger(SessionController.class); private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameController.class);
private final SessionRepository sessionRepository; private final BdsmGameRepository sessionRepository;
private final MitspielerRepository mitspielerRepository; private final MitspielerRepository mitspielerRepository;
private final AktiveSperreRepository aktiveSperreRepository; private final AktiveSperreRepository aktiveSperreRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final GameHistoryRepository gameHistoryRepository;
private final BdsmEinladungRepository einladungRepository;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public SessionController(SessionRepository sessionRepository, MitspielerRepository mitspielerRepository, public BdsmGameController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository,
AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository, AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository,
GameHistoryRepository gameHistoryRepository, BdsmEinladungRepository einladungRepository,
ObjectMapper objectMapper) { ObjectMapper objectMapper) {
this.sessionRepository = sessionRepository; this.sessionRepository = sessionRepository;
this.mitspielerRepository = mitspielerRepository; this.mitspielerRepository = mitspielerRepository;
this.aktiveSperreRepository = aktiveSperreRepository; this.aktiveSperreRepository = aktiveSperreRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.gameHistoryRepository = gameHistoryRepository;
this.einladungRepository = einladungRepository;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
} }
@GetMapping("/{sessionId}") @GetMapping("/{sessionId}")
public ResponseEntity<Session> getBySessionId(@PathVariable UUID sessionId) { public ResponseEntity<BdsmGame> getBySessionId(@PathVariable UUID sessionId) {
return sessionRepository.findById(sessionId) return sessionRepository.findById(sessionId)
.map(entity -> ResponseEntity.ok(toSession(entity))) .map(entity -> ResponseEntity.ok(toSession(entity)))
.orElse(ResponseEntity.noContent().build()); .orElse(ResponseEntity.noContent().build());
} }
@GetMapping @GetMapping
public ResponseEntity<Session> getByUserId(@RequestParam UUID userId) { public ResponseEntity<BdsmGame> getByUserId(@RequestParam UUID userId) {
return sessionRepository.findByUserId(userId) return sessionRepository.findByUserId(userId)
.map(entity -> ResponseEntity.ok(toSession(entity))) .map(entity -> ResponseEntity.ok(toSession(entity)))
.orElse(ResponseEntity.noContent().build()); .orElse(ResponseEntity.noContent().build());
} }
@PostMapping @PostMapping
public ResponseEntity<Void> create(@RequestBody Session session) { public ResponseEntity<Void> create(@RequestBody BdsmGame session) {
String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
UUID userId = userRepository.findByEmail(email).map(u -> u.getUserId()).orElse(null); UUID userId = userRepository.findByEmail(email).map(u -> u.getUserId()).orElse(null);
if (userId == null) return ResponseEntity.status(401).build(); if (userId == null) return ResponseEntity.status(401).build();
SessionEntity entity = new SessionEntity(); BdsmGameEntity entity = new BdsmGameEntity();
entity.setSessionId(UUID.randomUUID()); entity.setSessionId(UUID.randomUUID());
entity.setUserId(userId); entity.setUserId(userId);
entity.setAufgabenAufAktuellemLevel(0); entity.setAufgabenAufAktuellemLevel(0);
@@ -85,8 +101,16 @@ public class SessionController {
entity.setWahrscheinlichkeitStrafe(session.getWahrscheinlichkeitStrafe() != null ? session.getWahrscheinlichkeitStrafe() : 10); entity.setWahrscheinlichkeitStrafe(session.getWahrscheinlichkeitStrafe() != null ? session.getWahrscheinlichkeitStrafe() : 10);
entity.setZeitfaktorZeitstrafen(session.getZeitfaktorZeitstrafen() != null ? session.getZeitfaktorZeitstrafen() : 1.0); entity.setZeitfaktorZeitstrafen(session.getZeitfaktorZeitstrafen() != null ? session.getZeitfaktorZeitstrafen() : 1.0);
entity.setLevel(1); entity.setLevel(1);
entity.setSetupId(session.getSetupId());
sessionRepository.save(entity); sessionRepository.save(entity);
LOGGER.debug("Session gestartet [sessionId={}, userId={}, aufgabenProLevel={}, wahrscheinlichkeitStrafe={}%, wahrscheinlichkeitSperre={}%, zeitfaktorZeitstrafen={}]", // Akzeptierte Einladungen mit der neuen Session verknüpfen
if (session.getSetupId() != null) {
einladungRepository.findBySetupId(session.getSetupId()).stream()
.filter(e -> e.getStatus() == BdsmEinladungEntity.Status.ACCEPTED_OWN
|| e.getStatus() == BdsmEinladungEntity.Status.ACCEPTED_HOST)
.forEach(e -> e.setSessionId(entity.getSessionId()));
}
LOGGER.debug("BdsmGame gestartet [sessionId={}, userId={}, aufgabenProLevel={}, wahrscheinlichkeitStrafe={}%, wahrscheinlichkeitSperre={}%, zeitfaktorZeitstrafen={}]",
entity.getSessionId(), entity.getUserId(), entity.getAufgabenProLevel(), entity.getSessionId(), entity.getUserId(), entity.getAufgabenProLevel(),
entity.getWahrscheinlichkeitStrafe(), entity.getWahrscheinlichkeitSperre(), entity.getWahrscheinlichkeitStrafe(), entity.getWahrscheinlichkeitSperre(),
entity.getZeitfaktorZeitstrafen()); entity.getZeitfaktorZeitstrafen());
@@ -96,9 +120,20 @@ public class SessionController {
} }
@DeleteMapping @DeleteMapping
public ResponseEntity<Void> deleteSession(@RequestBody Session session) { public ResponseEntity<Void> deleteSession(@RequestBody BdsmGame session) {
return sessionRepository.findById(session.getSessionId()) return sessionRepository.findById(session.getSessionId())
.map(entity -> { .map(entity -> {
LocalDateTime endTime = LocalDateTime.now();
long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes();
GameHistoryEntity entry = new GameHistoryEntity();
entry.setGameType(GameType.BDSM);
entry.setStartTime(entity.getStartZeit());
entry.setEndTime(endTime);
entry.setDurationMinutes(durationMinutes);
entry.addParticipant(entity.getUserId(), GameRole.PLAYER);
gameHistoryRepository.save(entry);
aktiveSperreRepository.deleteAll(entity.getAktiveSperren()); aktiveSperreRepository.deleteAll(entity.getAktiveSperren());
mitspielerRepository.deleteAll(entity.getMitspieler()); mitspielerRepository.deleteAll(entity.getMitspieler());
sessionRepository.delete(entity); sessionRepository.delete(entity);
@@ -114,7 +149,7 @@ public class SessionController {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
String aufgaben = objectMapper.writeValueAsString(list); String aufgaben = objectMapper.writeValueAsString(list);
SessionEntity session = sessionRepository.findById(sessionId).orElse(null); BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) { if (session == null) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
@@ -130,12 +165,12 @@ public class SessionController {
@GetMapping("/{sessionId}/aufgaben/next") @GetMapping("/{sessionId}/aufgaben/next")
public ResponseEntity<AufgabeAnzeige> getNextAufgabe(@PathVariable UUID sessionId) { public ResponseEntity<AufgabeAnzeige> getNextAufgabe(@PathVariable UUID sessionId) {
try { try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null); BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) { if (session == null) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
session.setLetzteAktivitaet(LocalDateTime.now()); session.setLetzteAktivitaet(LocalDateTime.now());
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session); BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
AufgabeAnzeige next = durchfuehren.getNext(); AufgabeAnzeige next = durchfuehren.getNext();
session.setLevel(durchfuehren.getLevel()); session.setLevel(durchfuehren.getLevel());
session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel()); session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel());
@@ -165,7 +200,7 @@ public class SessionController {
|| mitspieler.getVerfuegbareWerkzeuge() == null || mitspieler.getVerfuegbareWerkzeuge().isEmpty()) { || mitspieler.getVerfuegbareWerkzeuge() == null || mitspieler.getVerfuegbareWerkzeuge().isEmpty()) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
SessionEntity session = sessionRepository.findById(sessionId).orElse(null); BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) { if (session == null) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
@@ -176,6 +211,8 @@ public class SessionController {
entity.setRollen(mitspieler.getRollen()); entity.setRollen(mitspieler.getRollen());
entity.setSpieltMit(mitspieler.getSpieltMit()); entity.setSpieltMit(mitspieler.getSpieltMit());
entity.setWerkzeuge(mitspieler.getVerfuegbareWerkzeuge()); entity.setWerkzeuge(mitspieler.getVerfuegbareWerkzeuge());
entity.setUserId(mitspieler.getUserId());
entity.setEigenesGeraet(mitspieler.isEigenesGeraet());
entity.setSession(session); entity.setSession(session);
mitspielerRepository.save(entity); mitspielerRepository.save(entity);
return ResponseEntity.accepted().build(); return ResponseEntity.accepted().build();
@@ -184,9 +221,9 @@ public class SessionController {
@GetMapping("/{sessionId}/finisher") @GetMapping("/{sessionId}/finisher")
public ResponseEntity<List<AufgabeAnzeige>> getFinisher(@PathVariable UUID sessionId) { public ResponseEntity<List<AufgabeAnzeige>> getFinisher(@PathVariable UUID sessionId) {
try { try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null); BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.badRequest().build(); if (session == null) return ResponseEntity.badRequest().build();
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session); BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
return ResponseEntity.ok(durchfuehren.getFinisher()); return ResponseEntity.ok(durchfuehren.getFinisher());
} catch (Exception exception) { } catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception); LOGGER.error(exception.getMessage(), exception);
@@ -197,9 +234,9 @@ public class SessionController {
@PostMapping("/{sessionId}/backToLevel5") @PostMapping("/{sessionId}/backToLevel5")
public ResponseEntity<Void> backToLevel5(@PathVariable UUID sessionId) { public ResponseEntity<Void> backToLevel5(@PathVariable UUID sessionId) {
try { try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null); BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.badRequest().build(); if (session == null) return ResponseEntity.badRequest().build();
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session); BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
durchfuehren.backToLvl5(); durchfuehren.backToLvl5();
session.setLevel(durchfuehren.getLevel()); session.setLevel(durchfuehren.getLevel());
session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel()); session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel());
@@ -211,8 +248,62 @@ public class SessionController {
} }
} }
private Session toSession(SessionEntity entity) { @GetMapping("/{sessionId}/mitspieler/me")
Session session = new Session(); public ResponseEntity<Map<String, Object>> getMeinMitspieler(@PathVariable UUID sessionId, Principal principal) {
UUID userId = userRepository.findByEmail(principal.getName()).map(u -> u.getUserId()).orElse(null);
if (userId == null) return ResponseEntity.status(401).build();
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
return session.getMitspieler().stream()
.filter(m -> userId.equals(m.getUserId()))
.findFirst()
.map(m -> {
Map<String, Object> result = new LinkedHashMap<>();
result.put("mitspielerId", m.getMitspielerId());
result.put("name", m.getName());
result.put("eigenesGeraet", m.isEigenesGeraet());
return ResponseEntity.ok(result);
})
.orElse(ResponseEntity.noContent().build());
}
record ActiveTaskRequest(String taskJson, LocalDateTime timerStartedAt) {}
record ActiveTaskResponse(String taskJson, Long elapsedSeconds) {}
@PutMapping("/{sessionId}/active-task")
public ResponseEntity<Void> setActiveTask(@PathVariable UUID sessionId, @RequestBody ActiveTaskRequest req) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
session.setActiveTaskJson(req.taskJson());
session.setTaskStartedAt(req.timerStartedAt());
sessionRepository.save(session);
return ResponseEntity.accepted().build();
}
@DeleteMapping("/{sessionId}/active-task")
public ResponseEntity<Void> clearActiveTask(@PathVariable UUID sessionId) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
session.setActiveTaskJson(null);
session.setTaskStartedAt(null);
sessionRepository.save(session);
return ResponseEntity.accepted().build();
}
@GetMapping("/{sessionId}/active-task")
public ResponseEntity<ActiveTaskResponse> getActiveTask(@PathVariable UUID sessionId) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
if (session.getActiveTaskJson() == null) return ResponseEntity.noContent().build();
Long elapsed = null;
if (session.getTaskStartedAt() != null) {
elapsed = Duration.between(session.getTaskStartedAt(), LocalDateTime.now()).getSeconds();
}
return ResponseEntity.ok(new ActiveTaskResponse(session.getActiveTaskJson(), elapsed));
}
private BdsmGame toSession(BdsmGameEntity entity) {
BdsmGame session = new BdsmGame();
session.setSessionId(entity.getSessionId()); session.setSessionId(entity.getSessionId());
session.setUserId(entity.getUserId()); session.setUserId(entity.getUserId());
session.setAufgabenProLevel(entity.getAufgabenProLevel()); session.setAufgabenProLevel(entity.getAufgabenProLevel());

View File

@@ -1,15 +1,15 @@
package de.oaa.xxx.session.controller; package de.oaa.xxx.games.bdsm.controller;
import de.oaa.xxx.session.AktiveSperre; import de.oaa.xxx.games.bdsm.AktiveSperre;
import de.oaa.xxx.session.Mitspieler; import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.session.entity.AktiveSperreEntity; import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import de.oaa.xxx.session.entity.SessionEntity; import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository; import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.session.repository.MitspielerRepository; import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.session.repository.SessionRepository; import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import de.oaa.xxx.session.sperre.SperreCallback; import de.oaa.xxx.games.bdsm.sperre.SperreCallback;
import de.oaa.xxx.session.sperre.SperreVerarbeiten; import de.oaa.xxx.games.bdsm.sperre.SperreVerarbeiten;
import de.oaa.xxx.session.sperre.SperrenVerlaengernCallback; import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -26,18 +26,18 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@RestController("sessionSperreController") @RestController("bdsmSperreController")
@RequestMapping("/session/sperre") @RequestMapping("/bdsm/sperre")
@Transactional @Transactional
public class SperreController { public class SperreController {
private static final Logger LOGGER = LoggerFactory.getLogger(SperreController.class); private static final Logger LOGGER = LoggerFactory.getLogger(SperreController.class);
private final SessionRepository sessionRepository; private final BdsmGameRepository sessionRepository;
private final MitspielerRepository mitspielerRepository; private final MitspielerRepository mitspielerRepository;
private final AktiveSperreRepository aktiveSperreRepository; private final AktiveSperreRepository aktiveSperreRepository;
public SperreController(SessionRepository sessionRepository, MitspielerRepository mitspielerRepository, public SperreController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository,
AktiveSperreRepository aktiveSperreRepository) { AktiveSperreRepository aktiveSperreRepository) {
this.sessionRepository = sessionRepository; this.sessionRepository = sessionRepository;
this.mitspielerRepository = mitspielerRepository; this.mitspielerRepository = mitspielerRepository;
@@ -78,7 +78,7 @@ public class SperreController {
@GetMapping("/aktive") @GetMapping("/aktive")
public ResponseEntity<List<AktiveSperre>> getAktiveSperren(@RequestParam UUID sessionId) { public ResponseEntity<List<AktiveSperre>> getAktiveSperren(@RequestParam UUID sessionId) {
try { try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null); BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.noContent().build(); if (session == null) return ResponseEntity.noContent().build();
List<Mitspieler> mitspielerList = session.getMitspieler().stream() List<Mitspieler> mitspielerList = session.getMitspieler().stream()
.map(m -> m.toMitspieler()) .map(m -> m.toMitspieler())

View File

@@ -1,8 +1,8 @@
package de.oaa.xxx.session.entity; package de.oaa.xxx.games.bdsm.entity;
import de.oaa.xxx.session.AktiveSperre; import de.oaa.xxx.games.bdsm.AktiveSperre;
import de.oaa.xxx.session.Mitspieler; import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.session.Werkzeug; import de.oaa.xxx.games.bdsm.Werkzeug;
import jakarta.persistence.CollectionTable; import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection; import jakarta.persistence.ElementCollection;
@@ -50,7 +50,7 @@ public class AktiveSperreEntity {
private String releaseText; private String releaseText;
@ManyToOne @ManyToOne
@JoinColumn(name = "sessionId", nullable = false) @JoinColumn(name = "sessionId", nullable = false)
private SessionEntity session; private BdsmGameEntity session;
public AktiveSperre toSperre(List<Mitspieler> mitspielerList) { public AktiveSperre toSperre(List<Mitspieler> mitspielerList) {
AktiveSperre sperre = new AktiveSperre(); AktiveSperre sperre = new AktiveSperre();

View File

@@ -0,0 +1,30 @@
package de.oaa.xxx.games.bdsm.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "bdsm_defaults")
public class BdsmDefaultsEntity {
@Id
@Column(name = "user_id")
private UUID userId;
@Column(length = 100)
private String spieltMit;
@Column(length = 200)
private String rollen;
@Column(length = 200)
private String werkzeuge;
}

View File

@@ -0,0 +1,50 @@
package de.oaa.xxx.games.bdsm.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "bdsm_einladung")
public class BdsmEinladungEntity {
public enum Status {
PENDING, ACCEPTED_OWN, ACCEPTED_HOST, DECLINED, CANCELLED
}
@Id
@Column
private UUID einladungId;
@Column(nullable = false)
private UUID setupId;
@Column
private UUID sessionId;
@Column(nullable = false)
private UUID inviterId;
@Column(nullable = false)
private UUID inviteeId;
@Column(nullable = false)
private int slotIndex;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private Status status;
@Column(nullable = false)
private LocalDateTime createdAt;
}

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session.entity; package de.oaa.xxx.games.bdsm.entity;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@@ -18,7 +18,7 @@ import java.util.UUID;
@Setter @Setter
@Entity @Entity
@Table(name = "session") @Table(name = "session")
public class SessionEntity { public class BdsmGameEntity {
@Id @Id
@Column @Column
@@ -47,10 +47,16 @@ public class SessionEntity {
private String aufgaben; private String aufgaben;
@Column @Column
private Double zeitfaktorZeitstrafen; private Double zeitfaktorZeitstrafen;
@Column(columnDefinition = "TEXT")
private String activeTaskJson;
@Column
private LocalDateTime taskStartedAt;
@Column
private UUID setupId;
@Override @Override
public String toString() { public String toString() {
return "SessionEntity[sessionId=" + sessionId + ", userId=" + userId return "BdsmGameEntity[sessionId=" + sessionId + ", userId=" + userId
+ ", level=" + level + ", aufgaben=" + aufgabenAufAktuellemLevel + "/" + aufgabenProLevel + ", level=" + level + ", aufgaben=" + aufgabenAufAktuellemLevel + "/" + aufgabenProLevel
+ ", pStrafe=" + wahrscheinlichkeitStrafe + "%, pSperre=" + wahrscheinlichkeitSperre + "%" + ", pStrafe=" + wahrscheinlichkeitStrafe + "%, pSperre=" + wahrscheinlichkeitSperre + "%"
+ ", zeitfaktor=" + zeitfaktorZeitstrafen + ", start=" + startZeit + "]"; + ", zeitfaktor=" + zeitfaktorZeitstrafen + ", start=" + startZeit + "]";

View File

@@ -1,77 +1,83 @@
package de.oaa.xxx.session.entity; package de.oaa.xxx.games.bdsm.entity;
import de.oaa.xxx.session.GeschlechtEnum; import de.oaa.xxx.games.bdsm.GeschlechtEnum;
import de.oaa.xxx.session.Mitspieler; import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.session.RolleEnum; import de.oaa.xxx.games.bdsm.RolleEnum;
import de.oaa.xxx.session.Werkzeug; import de.oaa.xxx.games.bdsm.Werkzeug;
import jakarta.persistence.CollectionTable; import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection; import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated; import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "mitspieler") @Table(name = "mitspieler")
public class MitspielerEntity { public class MitspielerEntity {
@Id @Id
@Column @Column
private UUID mitspielerId; private UUID mitspielerId;
@Column @Column
private String name; private UUID userId;
@Enumerated(EnumType.STRING) @Column
@Column private boolean eigenesGeraet;
private GeschlechtEnum geschlecht; @Column
@Enumerated(EnumType.STRING) private String name;
@ElementCollection(targetClass = Werkzeug.class, fetch = FetchType.EAGER) @Enumerated(EnumType.STRING)
@CollectionTable(name = "mitspieler_werkzeuge", joinColumns = @JoinColumn(name = "mitspielerId")) @Column
@Column(name = "werkzeug") private GeschlechtEnum geschlecht;
private List<Werkzeug> werkzeuge = new ArrayList<>(); @Enumerated(EnumType.STRING)
@Enumerated(EnumType.STRING) @ElementCollection(targetClass = Werkzeug.class, fetch = FetchType.EAGER)
@ElementCollection(targetClass = GeschlechtEnum.class, fetch = FetchType.EAGER) @CollectionTable(name = "mitspieler_werkzeuge", joinColumns = @JoinColumn(name = "mitspielerId"))
@CollectionTable(name = "mitspieler_spieltMit", joinColumns = @JoinColumn(name = "mitspielerId")) @Column(name = "werkzeug")
@Column(name = "geschlecht") private List<Werkzeug> werkzeuge = new ArrayList<>();
private List<GeschlechtEnum> spieltMit = new ArrayList<>(); @Enumerated(EnumType.STRING)
@Enumerated(EnumType.STRING) @ElementCollection(targetClass = GeschlechtEnum.class, fetch = FetchType.EAGER)
@ElementCollection(targetClass = RolleEnum.class, fetch = FetchType.EAGER) @CollectionTable(name = "mitspieler_spieltMit", joinColumns = @JoinColumn(name = "mitspielerId"))
@CollectionTable(name = "mitspieler_rollen", joinColumns = @JoinColumn(name = "mitspielerId")) @Column(name = "geschlecht")
@Column(name = "rolle") private List<GeschlechtEnum> spieltMit = new ArrayList<>();
private List<RolleEnum> rollen = new ArrayList<>(); @Enumerated(EnumType.STRING)
@ManyToOne @ElementCollection(targetClass = RolleEnum.class, fetch = FetchType.EAGER)
@JoinColumn(name = "sessionId", nullable = false) @CollectionTable(name = "mitspieler_rollen", joinColumns = @JoinColumn(name = "mitspielerId"))
private SessionEntity session; @Column(name = "rolle")
@OneToMany(mappedBy = "mitspieler", fetch = FetchType.EAGER) private List<RolleEnum> rollen = new ArrayList<>();
private List<AktiveSperreEntity> aktiveSperren = new ArrayList<>(); @ManyToOne
@JoinColumn(name = "sessionId", nullable = false)
@Override private BdsmGameEntity session;
public String toString() { @OneToMany(mappedBy = "mitspieler", fetch = FetchType.EAGER)
return "MitspielerEntity[mitspielerId=" + mitspielerId + ", name=" + name private List<AktiveSperreEntity> aktiveSperren = new ArrayList<>();
+ ", geschlecht=" + geschlecht + ", rollen=" + rollen + ", werkzeuge=" + werkzeuge + "]";
} @Override
public String toString() {
public Mitspieler toMitspieler() { return "MitspielerEntity[mitspielerId=" + mitspielerId + ", name=" + name
Mitspieler mitspieler = new Mitspieler(); + ", geschlecht=" + geschlecht + ", rollen=" + rollen + ", werkzeuge=" + werkzeuge + "]";
mitspieler.setGeschlecht(geschlecht); }
mitspieler.setId(mitspielerId);
mitspieler.setName(name); public Mitspieler toMitspieler() {
mitspieler.setRollen(rollen); Mitspieler mitspieler = new Mitspieler();
mitspieler.setSpieltMit(spieltMit); mitspieler.setGeschlecht(geschlecht);
mitspieler.setVerfuegbareWerkzeuge(new ArrayList<>(werkzeuge)); mitspieler.setId(mitspielerId);
return mitspieler; mitspieler.setUserId(userId);
} mitspieler.setEigenesGeraet(eigenesGeraet);
} mitspieler.setName(name);
mitspieler.setRollen(rollen);
mitspieler.setSpieltMit(spieltMit);
mitspieler.setVerfuegbareWerkzeuge(new ArrayList<>(werkzeuge));
return mitspieler;
}
}

View File

@@ -1,6 +1,6 @@
package de.oaa.xxx.session.repository; package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.session.entity.AktiveSperreEntity; import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;

View File

@@ -0,0 +1,11 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface BdsmDefaultsRepository extends JpaRepository<BdsmDefaultsEntity, UUID> {
Optional<BdsmDefaultsEntity> findByUserId(UUID userId);
}

View File

@@ -0,0 +1,17 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity.Status;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface BdsmEinladungRepository extends JpaRepository<BdsmEinladungEntity, UUID> {
List<BdsmEinladungEntity> findBySetupId(UUID setupId);
List<BdsmEinladungEntity> findByInviteeIdAndStatus(UUID inviteeId, Status status);
List<BdsmEinladungEntity> findByInviterIdAndStatus(UUID inviterId, Status status);
}

View File

@@ -0,0 +1,12 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface BdsmGameRepository extends JpaRepository<BdsmGameEntity, UUID> {
Optional<BdsmGameEntity> findByUserId(UUID userId);
}

View File

@@ -1,6 +1,6 @@
package de.oaa.xxx.session.repository; package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.session.entity.MitspielerEntity; import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID; import java.util.UUID;

View File

@@ -1,6 +1,6 @@
package de.oaa.xxx.session.sperre; package de.oaa.xxx.games.bdsm.sperre;
import de.oaa.xxx.session.Callback; import de.oaa.xxx.games.bdsm.Callback;
import java.util.UUID; import java.util.UUID;

View File

@@ -1,14 +1,14 @@
package de.oaa.xxx.session.sperre; package de.oaa.xxx.games.bdsm.sperre;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.session.aufgaben.AufgabenList; import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
import de.oaa.xxx.session.aufgaben.Sperre; import de.oaa.xxx.games.bdsm.aufgaben.Sperre;
import de.oaa.xxx.session.entity.AktiveSperreEntity; import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import de.oaa.xxx.session.entity.MitspielerEntity; import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import de.oaa.xxx.session.entity.SessionEntity; import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository; import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.session.repository.MitspielerRepository; import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.session.repository.SessionRepository; import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Optional; import java.util.Optional;
@@ -19,9 +19,9 @@ public class SperreVerarbeiten {
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
public void sperreAnwenden(SperreCallback callback, SessionRepository sessionRepository, public void sperreAnwenden(SperreCallback callback, BdsmGameRepository sessionRepository,
MitspielerRepository mitspielerRepository, AktiveSperreRepository sperreRepository) throws Exception { MitspielerRepository mitspielerRepository, AktiveSperreRepository sperreRepository) throws Exception {
SessionEntity session = sessionRepository.findById(callback.getSessionId()).orElse(null); BdsmGameEntity session = sessionRepository.findById(callback.getSessionId()).orElse(null);
MitspielerEntity mitspieler = mitspielerRepository.findById(callback.getSpielerId()).orElse(null); MitspielerEntity mitspieler = mitspielerRepository.findById(callback.getSpielerId()).orElse(null);
if (session != null) { if (session != null) {
AufgabenList aufgaben = objectMapper.readValue(session.getAufgaben(), AufgabenList.class); AufgabenList aufgaben = objectMapper.readValue(session.getAufgaben(), AufgabenList.class);
@@ -56,7 +56,7 @@ public class SperreVerarbeiten {
sperreRepository.save(verlaengern); sperreRepository.save(verlaengern);
} }
private void fill(SperreCallback callback, SessionEntity session, MitspielerEntity mitspieler, private void fill(SperreCallback callback, BdsmGameEntity session, MitspielerEntity mitspieler,
Sperre sperre, AktiveSperreEntity aktiv) { Sperre sperre, AktiveSperreEntity aktiv) {
aktiv.setAktiveSperreId(UUID.randomUUID()); aktiv.setAktiveSperreId(UUID.randomUUID());
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
@@ -70,7 +70,7 @@ public class SperreVerarbeiten {
aktiv.setReleaseText(callback.getReleaseText()); aktiv.setReleaseText(callback.getReleaseText());
} }
private Integer berechneDauer(SessionEntity session, Sperre sperre) { private Integer berechneDauer(BdsmGameEntity session, Sperre sperre) {
Integer minuten = 30; Integer minuten = 30;
if (sperre.getMinutenVon() != null) { if (sperre.getMinutenVon() != null) {
if (sperre.getMinutenBis() != null) { if (sperre.getMinutenBis() != null) {

View File

@@ -1,6 +1,6 @@
package de.oaa.xxx.session.sperre; package de.oaa.xxx.games.bdsm.sperre;
import de.oaa.xxx.session.Callback; import de.oaa.xxx.games.bdsm.Callback;
import java.util.UUID; import java.util.UUID;

View File

@@ -22,9 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository; import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
import de.oaa.xxx.social.SseService; import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
@RestController @RestController
@@ -34,8 +32,7 @@ public class LockeeInvitationController {
private final LockeeInvitationRepository lockeeInvitationRepository; private final LockeeInvitationRepository lockeeInvitationRepository;
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final MessageRepository messageRepository; private final SystemMessageService systemMessageService;
private final SseService sseService;
@Value("${app.base-url:http://localhost:8080}") @Value("${app.base-url:http://localhost:8080}")
private String baseUrl; private String baseUrl;
@@ -45,27 +42,15 @@ public class LockeeInvitationController {
public LockeeInvitationController(LockeeInvitationRepository lockeeInvitationRepository, public LockeeInvitationController(LockeeInvitationRepository lockeeInvitationRepository,
CardlockRepository cardlockRepository, CardlockRepository cardlockRepository,
UserRepository userRepository, UserRepository userRepository,
MessageRepository messageRepository, SystemMessageService systemMessageService) {
SseService sseService) {
this.lockeeInvitationRepository = lockeeInvitationRepository; this.lockeeInvitationRepository = lockeeInvitationRepository;
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.messageRepository = messageRepository; this.systemMessageService = systemMessageService;
this.sseService = sseService;
} }
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl) { private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl, de.oaa.xxx.social.entity.MessageCause cause) {
MessageEntity msg = new MessageEntity(); systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(senderId);
msg.setReceiverId(receiverId);
msg.setText(text);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
if (targetUrl != null) msg.setTargetUrl(targetUrl);
messageRepository.save(msg);
long unread = messageRepository.countByReceiverIdAndSystemMessageAndReadAtIsNull(receiverId, true);
sseService.push(receiverId, "NOTIFICATION", java.util.Map.of("unreadCount", unread, "text", text));
} }
private String generateUnlockCode(int lines) { private String generateUnlockCode(int lines) {
@@ -154,7 +139,7 @@ public class LockeeInvitationController {
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock"; String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getLockeeUserId(), sendMessage(myId, inv.getLockeeUserId(),
me.getName() + " hat die Lockee-Einladung für das Lock „" + lockName + "\" zurückgezogen.", me.getName() + " hat die Lockee-Einladung für das Lock „" + lockName + "\" zurückgezogen.",
null); null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
} }
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
@@ -248,7 +233,7 @@ public class LockeeInvitationController {
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock"; String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getKeyholderUserId(), sendMessage(myId, inv.getKeyholderUserId(),
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" angenommen.", me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" angenommen.",
"/keyholder.html"); "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"lockId", lock.getLockId().toString(), "lockId", lock.getLockId().toString(),
@@ -278,7 +263,7 @@ public class LockeeInvitationController {
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock"; String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getKeyholderUserId(), sendMessage(myId, inv.getKeyholderUserId(),
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" abgelehnt.", me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" abgelehnt.",
null); null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
} }
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();

View File

@@ -41,7 +41,7 @@ import de.oaa.xxx.games.chastity.KeyholderInvitationEntity;
import de.oaa.xxx.games.chastity.KeyholderInvitationRepository; import de.oaa.xxx.games.chastity.KeyholderInvitationRepository;
import de.oaa.xxx.games.chastity.LockeeInvitationEntity; import de.oaa.xxx.games.chastity.LockeeInvitationEntity;
import de.oaa.xxx.games.chastity.LockeeInvitationRepository; import de.oaa.xxx.games.chastity.LockeeInvitationRepository;
import de.oaa.xxx.games.chastity.history.LockHistoryRepository; import de.oaa.xxx.games.history.GameHistoryRepository;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity; import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository; import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task; import de.oaa.xxx.games.chastity.tasks.Task;
@@ -49,9 +49,7 @@ import de.oaa.xxx.games.chastity.verification.VerificationEntity;
import de.oaa.xxx.games.chastity.verification.VerificationRepository; import de.oaa.xxx.games.chastity.verification.VerificationRepository;
import de.oaa.xxx.games.chastity.verification.VerificationVoteEntity; import de.oaa.xxx.games.chastity.verification.VerificationVoteEntity;
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository; import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
import de.oaa.xxx.social.SseService; import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
@RestController @RestController
@@ -65,15 +63,14 @@ public class CardLockController {
private final VerificationRepository verificationRepository; private final VerificationRepository verificationRepository;
private final VerificationVoteRepository verificationVoteRepository; private final VerificationVoteRepository verificationVoteRepository;
private final HygieneViolationRepository hygieneViolationRepository; private final HygieneViolationRepository hygieneViolationRepository;
private final MessageRepository messageRepository;
private final LockeeInvitationRepository lockeeInvitationRepository; private final LockeeInvitationRepository lockeeInvitationRepository;
private final AssignedTaskRepository assignedTaskRepository; private final AssignedTaskRepository assignedTaskRepository;
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository; private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
private final CommunityTaskVoteRepository communityTaskVoteRepository; private final CommunityTaskVoteRepository communityTaskVoteRepository;
private final UnlockCodeHistoryRepository unlockCodeHistoryRepository; private final UnlockCodeHistoryRepository unlockCodeHistoryRepository;
private final UnlockCodeHistoryService unlockCodeHistoryService; private final UnlockCodeHistoryService unlockCodeHistoryService;
private final LockHistoryRepository lockHistoryRepository; private final GameHistoryRepository gameHistoryRepository;
private final SseService sseService; private final SystemMessageService systemMessageService;
@Value("${app.base-url:http://localhost:8080}") @Value("${app.base-url:http://localhost:8080}")
private String baseUrl; private String baseUrl;
@@ -85,15 +82,14 @@ public class CardLockController {
VerificationRepository verificationRepository, VerificationRepository verificationRepository,
VerificationVoteRepository verificationVoteRepository, VerificationVoteRepository verificationVoteRepository,
HygieneViolationRepository hygieneViolationRepository, HygieneViolationRepository hygieneViolationRepository,
MessageRepository messageRepository,
LockeeInvitationRepository lockeeInvitationRepository, LockeeInvitationRepository lockeeInvitationRepository,
AssignedTaskRepository assignedTaskRepository, AssignedTaskRepository assignedTaskRepository,
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository, KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
CommunityTaskVoteRepository communityTaskVoteRepository, CommunityTaskVoteRepository communityTaskVoteRepository,
UnlockCodeHistoryRepository unlockCodeHistoryRepository, UnlockCodeHistoryRepository unlockCodeHistoryRepository,
UnlockCodeHistoryService unlockCodeHistoryService, UnlockCodeHistoryService unlockCodeHistoryService,
LockHistoryRepository lockHistoryRepository, GameHistoryRepository gameHistoryRepository,
SseService sseService) { SystemMessageService systemMessageService) {
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.cardLockRepository = cardLockRepository; this.cardLockRepository = cardLockRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
@@ -101,15 +97,14 @@ public class CardLockController {
this.verificationRepository = verificationRepository; this.verificationRepository = verificationRepository;
this.verificationVoteRepository = verificationVoteRepository; this.verificationVoteRepository = verificationVoteRepository;
this.hygieneViolationRepository = hygieneViolationRepository; this.hygieneViolationRepository = hygieneViolationRepository;
this.messageRepository = messageRepository;
this.lockeeInvitationRepository = lockeeInvitationRepository; this.lockeeInvitationRepository = lockeeInvitationRepository;
this.assignedTaskRepository = assignedTaskRepository; this.assignedTaskRepository = assignedTaskRepository;
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository; this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
this.communityTaskVoteRepository = communityTaskVoteRepository; this.communityTaskVoteRepository = communityTaskVoteRepository;
this.unlockCodeHistoryRepository = unlockCodeHistoryRepository; this.unlockCodeHistoryRepository = unlockCodeHistoryRepository;
this.unlockCodeHistoryService = unlockCodeHistoryService; this.unlockCodeHistoryService = unlockCodeHistoryService;
this.lockHistoryRepository = lockHistoryRepository; this.gameHistoryRepository = gameHistoryRepository;
this.sseService = sseService; this.systemMessageService = systemMessageService;
} }
record CreateCardLockRequest( record CreateCardLockRequest(
@@ -194,7 +189,7 @@ public class CardLockController {
String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock"; String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock";
sendMessage(myId, lockee.getUserId(), sendMessage(myId, lockee.getUserId(),
me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.", me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.",
"/einladungen.html"); "/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"lockId", lock.getLockId().toString(), "lockId", lock.getLockId().toString(),
@@ -257,7 +252,7 @@ public class CardLockController {
String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock"; String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock";
sendMessage(me.getUserId(), kh.getUserId(), sendMessage(me.getUserId(), kh.getUserId(),
me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.", me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.",
"/einladungen.html"); "/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
keyholderPending = true; keyholderPending = true;
} }
@@ -282,7 +277,7 @@ public class CardLockController {
var l = lockOpt.get(); var l = lockOpt.get();
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, lockHistoryRepository, userRepository); CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository);
CardDTO dto = service.getNextCard(); CardDTO dto = service.getNextCard();
if (dto == null) return ResponseEntity.status(409).body(Map.of("error", "Keine Karte verfügbar")); if (dto == null) return ResponseEntity.status(409).body(Map.of("error", "Keine Karte verfügbar"));
@@ -300,7 +295,7 @@ public class CardLockController {
userRepository.findById(l.getKeyholder()).ifPresent(kh -> userRepository.findById(l.getKeyholder()).ifPresent(kh ->
sendMessage(l.getLockee(), kh.getUserId(), sendMessage(l.getLockee(), kh.getUserId(),
"Deine Lockee hat eine Aufgaben-Karte gezogen wähle eine Aufgabe aus.", "Deine Lockee hat eine Aufgaben-Karte gezogen wähle eine Aufgabe aus.",
"/keyholder.html")); "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE));
taskPending = "KEYHOLDER"; taskPending = "KEYHOLDER";
} else if ("COMMUNITY".equals(l.getTaskCardMode())) { } else if ("COMMUNITY".equals(l.getTaskCardMode())) {
@@ -320,9 +315,14 @@ public class CardLockController {
result.put("unlockCode", dto.unlockCode() != null ? dto.unlockCode() : ""); result.put("unlockCode", dto.unlockCode() != null ? dto.unlockCode() : "");
if (taskPending != null) result.put("taskPending", taskPending); if (taskPending != null) result.put("taskPending", taskPending);
// Grüne Karte → Entsperrcode-Historie speichern // Grüne Karte → Entsperrcode-Historie speichern + Keyholder benachrichtigen
if (dto.unlockCode() != null && !dto.unlockCode().isBlank()) { if (dto.unlockCode() != null && !dto.unlockCode().isBlank()) {
unlockCodeHistoryService.save(myId, l.getLockId(), l.getName(), dto.unlockCode(), "GREEN_CARD"); unlockCodeHistoryService.save(myId, l.getLockId(), l.getName(), dto.unlockCode(), "GREEN_CARD");
if (l.getKeyholder() != null) {
sendMessage(myId, l.getKeyholder(),
meOpt.get().getName() + " hat die grüne Karte gezogen! Der Entsperrcode wurde angezeigt.",
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
}
} }
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
@@ -417,7 +417,7 @@ public class CardLockController {
var l = lockOpt.get(); var l = lockOpt.get();
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, lockHistoryRepository, userRepository); CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository);
service.clearTask(); service.clearTask();
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -434,8 +434,16 @@ public class CardLockController {
var l = lockOpt.get(); var l = lockOpt.get();
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, lockHistoryRepository, userRepository); CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository);
service.putBackGreen(); service.putBackGreen();
// Grüne Karte zurückgelegt → Keyholder benachrichtigen
if (l.getKeyholder() != null) {
sendMessage(myId, l.getKeyholder(),
meOpt.get().getName() + " hat die grüne Karte zurückgelegt und bleibt im Lock.",
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
}
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -557,7 +565,7 @@ public class CardLockController {
lockDirty = true; lockDirty = true;
sendMessage(l.getKeyholder(), l.getLockee(), sendMessage(l.getKeyholder(), l.getLockee(),
"Die dir gestellte Aufgabe ist abgelaufen, ohne dass du reagiert hast. Die Strafe wurde automatisch angewendet.", "Die dir gestellte Aufgabe ist abgelaufen, ohne dass du reagiert hast. Die Strafe wurde automatisch angewendet.",
"/activelock.html?lockId=" + l.getLockId()); "/activelock.html?lockId=" + l.getLockId(), de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
} }
if (lockDirty) cardlockRepository.save(l); if (lockDirty) cardlockRepository.save(l);
@@ -695,7 +703,7 @@ public class CardLockController {
var lockee = meOpt.get(); var lockee = meOpt.get();
sendMessage(myId, lock.getKeyholder(), sendMessage(myId, lock.getKeyholder(),
"📸 " + lockee.getName() + " hat eine Verifikation eingereicht.", "📸 " + lockee.getName() + " hat eine Verifikation eingereicht.",
"/keyholder.html"); "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
} }
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
@@ -793,14 +801,9 @@ public class CardLockController {
if (lockOpt.isPresent()) { if (lockOpt.isPresent()) {
var lock = lockOpt.get(); var lock = lockOpt.get();
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock"; String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
MessageEntity msg = new MessageEntity(); sendMessage(myId, lock.getLockee(),
msg.setMessageId(UUID.randomUUID()); me.getName() + " hat die Einladung als Keyholder*In für das Lock „" + lockName + "\" abgelehnt.",
msg.setSenderId(myId); null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
msg.setReceiverId(lock.getLockee());
msg.setText(me.getName() + " hat die Einladung als Keyholder*In für das Lock „" + lockName + "\" abgelehnt.");
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg);
} }
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
@@ -855,14 +858,9 @@ public class CardLockController {
String lockName = lockOpt.get().getName() != null && !lockOpt.get().getName().isBlank() String lockName = lockOpt.get().getName() != null && !lockOpt.get().getName().isBlank()
? lockOpt.get().getName() : "Unbenanntes Lock"; ? lockOpt.get().getName() : "Unbenanntes Lock";
MessageEntity msg = new MessageEntity(); sendMessage(myId, inv.getKeyholderUserId(),
msg.setMessageId(UUID.randomUUID()); me.getName() + " hat die Keyholder-Einladung für das Lock „" + lockName + "\" zurückgezogen.",
msg.setSenderId(myId); null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
msg.setReceiverId(inv.getKeyholderUserId());
msg.setText(me.getName() + " hat die Keyholder-Einladung für das Lock „" + lockName + "\" zurückgezogen.");
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -1069,7 +1067,7 @@ public class CardLockController {
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
// Entsperrung protokollieren (History + XP) gültig nur wenn Keyholder vorhanden und kein Auto-Notfall // Entsperrung protokollieren (History + XP) gültig nur wenn Keyholder vorhanden und kein Auto-Notfall
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, lockHistoryRepository, userRepository); CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository);
service.unlock(l.getUnlockCode()); service.unlock(l.getUnlockCode());
var verifications = verificationRepository.findByLockId(lockId); var verifications = verificationRepository.findByLockId(lockId);
@@ -1124,14 +1122,8 @@ public class CardLockController {
? me.getName() + " hat " + toAdd.size() + " Karte(n) zu deinem Lock hinzugefügt: " + detail + "." ? me.getName() + " hat " + toAdd.size() + " Karte(n) zu deinem Lock hinzugefügt: " + detail + "."
: me.getName() + " hat Karten zu deinem Lock hinzugefügt."; : me.getName() + " hat Karten zu deinem Lock hinzugefügt.";
MessageEntity msg = new MessageEntity(); sendMessage(myId, l.getLockee(), msgText, "/activelock.html?lockId=" + lockId,
msg.setMessageId(UUID.randomUUID()); de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
msg.setSenderId(myId);
msg.setReceiverId(l.getLockee());
msg.setText(msgText);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -1189,14 +1181,8 @@ public class CardLockController {
? me.getName() + " hat " + removed.size() + " Karte(n) aus deinem Lock entfernt: " + detail + "." ? me.getName() + " hat " + removed.size() + " Karte(n) aus deinem Lock entfernt: " + detail + "."
: me.getName() + " hat Karten aus deinem Lock entfernt."; : me.getName() + " hat Karten aus deinem Lock entfernt.";
MessageEntity msg = new MessageEntity(); sendMessage(myId, l.getLockee(), msgText, "/activelock.html?lockId=" + lockId,
msg.setMessageId(UUID.randomUUID()); de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
msg.setSenderId(myId);
msg.setReceiverId(l.getLockee());
msg.setText(msgText);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -1221,19 +1207,8 @@ public class CardLockController {
} }
} }
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl) { private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl, de.oaa.xxx.social.entity.MessageCause cause) {
if (senderId == null || receiverId == null) return; systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(senderId);
msg.setReceiverId(receiverId);
msg.setText(text);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
if (targetUrl != null) msg.setTargetUrl(targetUrl);
messageRepository.save(msg);
long unread = messageRepository.countByReceiverIdAndSystemMessageAndReadAtIsNull(receiverId, true);
sseService.push(receiverId, "NOTIFICATION", java.util.Map.of("unreadCount", unread, "text", text));
} }
@GetMapping("/cardlock/unlock-history") @GetMapping("/cardlock/unlock-history")
@@ -1305,7 +1280,7 @@ public class CardLockController {
sendMessage(me.getUserId(), l.getLockee(), sendMessage(me.getUserId(), l.getLockee(),
me.getName() + " hat dir eine Aufgabe gestellt. Du hast " + me.getName() + " hat dir eine Aufgabe gestellt. Du hast " +
req.acceptDeadlineMinutes() + " Minuten, um sie anzunehmen.", req.acceptDeadlineMinutes() + " Minuten, um sie anzunehmen.",
"/activelock.html?lockId=" + lockId); "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -1365,7 +1340,7 @@ public class CardLockController {
assignedTaskRepository.save(task); assignedTaskRepository.save(task);
cardlockRepository.save(l); cardlockRepository.save(l);
sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe angenommen.", "/keyholder.html"); sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe angenommen.", "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -1397,7 +1372,7 @@ public class CardLockController {
assignedTaskRepository.save(task); assignedTaskRepository.save(task);
cardlockRepository.save(l); cardlockRepository.save(l);
sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe abgelehnt. Die Strafe wurde angewendet.", "/keyholder.html"); sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe abgelehnt. Die Strafe wurde angewendet.", "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -1465,7 +1440,7 @@ public class CardLockController {
until.toLocalDate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")) + until.toLocalDate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")) +
" " + until.toLocalTime().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm")) + " " + until.toLocalTime().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm")) +
" Uhr eingefroren.", " Uhr eingefroren.",
"/activelock.html?lockId=" + lockId); "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -1490,7 +1465,7 @@ public class CardLockController {
cardlockRepository.save(l); cardlockRepository.save(l);
sendMessage(myId, l.getLockee(), me.getName() + " hat dein Lock wieder entfroren.", sendMessage(myId, l.getLockee(), me.getName() + " hat dein Lock wieder entfroren.",
"/activelock.html?lockId=" + lockId); "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -1512,7 +1487,7 @@ public class CardLockController {
sendMessage(myId, l.getLockee(), sendMessage(myId, l.getLockee(),
"Dein Keyholder hat das Lock freigeschaltet. Du erhältst beim nächsten Laden deinen Entsperrcode.", "Dein Keyholder hat das Lock freigeschaltet. Du erhältst beim nächsten Laden deinen Entsperrcode.",
"/activelock.html?lockId=" + lockId); "/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@@ -1542,7 +1517,7 @@ public class CardLockController {
// Keyholderin benachrichtigen // Keyholderin benachrichtigen
sendMessage(myId, l.getKeyholder(), sendMessage(myId, l.getKeyholder(),
"⚠️ NOTFALL: " + me.getName() + " bittet dringend um Freigabe des Locks. Bitte reagiere innerhalb einer Stunde, sonst öffnet sich das Lock automatisch.", "⚠️ NOTFALL: " + me.getName() + " bittet dringend um Freigabe des Locks. Bitte reagiere innerhalb einer Stunde, sonst öffnet sich das Lock automatisch.",
"/keyholder.html"); "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.EMERGENCY);
} }
cardlockRepository.save(l); cardlockRepository.save(l);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();

View File

@@ -11,10 +11,9 @@ import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import de.oaa.xxx.games.chastity.LockType;
import de.oaa.xxx.games.chastity.ProcessLock; import de.oaa.xxx.games.chastity.ProcessLock;
import de.oaa.xxx.games.chastity.history.LockHistoryEntity; import de.oaa.xxx.games.history.GameHistoryEntity;
import de.oaa.xxx.games.chastity.history.LockHistoryRepository; import de.oaa.xxx.games.history.GameHistoryRepository;
import de.oaa.xxx.games.chastity.tasks.Task; import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.verification.VerificationEntity; import de.oaa.xxx.games.chastity.verification.VerificationEntity;
import de.oaa.xxx.games.chastity.verification.VerificationRepository; import de.oaa.xxx.games.chastity.verification.VerificationRepository;
@@ -29,15 +28,15 @@ public class CardLockService extends ProcessLock {
private VerificationRepository verificationRepository; private VerificationRepository verificationRepository;
private VerificationVoteRepository verificationVoteRepository; private VerificationVoteRepository verificationVoteRepository;
private CardLockRepository cardLockRepository; private CardLockRepository cardLockRepository;
private LockHistoryRepository lockHistoryRepository; private GameHistoryRepository gameHistoryRepository;
private UserRepository userRepository; private UserRepository userRepository;
public CardLockService(CardLockEntity lock, VerificationRepository verificationRepository, VerificationVoteRepository verificationVoteRepository, CardLockRepository cardLockRepository, LockHistoryRepository lockHistoryRepository, UserRepository userRepository) { public CardLockService(CardLockEntity lock, VerificationRepository verificationRepository, VerificationVoteRepository verificationVoteRepository, CardLockRepository cardLockRepository, GameHistoryRepository gameHistoryRepository, UserRepository userRepository) {
this.lock = lock; this.lock = lock;
this.verificationRepository = verificationRepository; this.verificationRepository = verificationRepository;
this.verificationVoteRepository = verificationVoteRepository; this.verificationVoteRepository = verificationVoteRepository;
this.cardLockRepository = cardLockRepository; this.cardLockRepository = cardLockRepository;
this.lockHistoryRepository = lockHistoryRepository; this.gameHistoryRepository = gameHistoryRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
} }
@@ -101,28 +100,30 @@ public class CardLockService extends ProcessLock {
public void unlock(String unlockCode) { public void unlock(String unlockCode) {
this.lock.setUnlockTime(LocalDateTime.now()); this.lock.setUnlockTime(LocalDateTime.now());
// Self-Lock oder automatische Entsperrung ohne Keyholder-Zustimmung → ungültig boolean valid = true;
boolean valid = lock.getKeyholder() != null && !lock.isEmergencyAutoUnlocked(); if (lock.isEmergencyAutoUnlocked()) {
if (!this.lock.isTestLock()) { valid = false;
if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) { LOGGER.debug("Lock invalid - Emergency Auto-Unlock (1h timer)");
Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream() }
.filter(verification -> isValid(verification)) if (lock.isTestLock()) {
.map(verification -> verification.getVerificationTime().toLocalDate()) valid = false;
.collect(Collectors.toSet()); } else if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) {
Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream()
LocalDate current = this.lock.getStartTime().toLocalDate(); .filter(verification -> isValid(verification))
LocalDate last = this.lock.getUnlockTime().toLocalDate().minusDays(1); .map(verification -> verification.getVerificationTime().toLocalDate())
.collect(Collectors.toSet());
while (!current.isAfter(last)) {
if (!verifications.contains(current)) { LocalDate current = this.lock.getStartTime().toLocalDate();
valid = false; LocalDate last = this.lock.getUnlockTime().toLocalDate().minusDays(1);
break;
} while (!current.isAfter(last)) {
current = current.plusDays(1); if (!verifications.contains(current)) {
valid = false;
LOGGER.debug("Lock invalid - no daily verification on %s", current.toString());
break;
} }
current = current.plusDays(1);
} }
} }
lock.setUnlockTime(LocalDateTime.now()); lock.setUnlockTime(LocalDateTime.now());
@@ -132,31 +133,18 @@ public class CardLockService extends ProcessLock {
if (valid) { if (valid) {
long durationMinutes = Duration.between(lock.getStartTime(), lock.getUnlockTime()).toMinutes(); long durationMinutes = Duration.between(lock.getStartTime(), lock.getUnlockTime()).toMinutes();
// Eintrag für den Lockee // Gemeinsamer History-Eintrag mit Teilnehmerliste
LockHistoryEntity lockeeEntry = new LockHistoryEntity(); GameHistoryEntity entry = new GameHistoryEntity();
lockeeEntry.setUserId(lock.getLockee()); entry.setGameType(de.oaa.xxx.games.history.GameType.CARDLOCK);
lockeeEntry.setLockedBy(lock.getKeyholder()); entry.setGameName(lock.getName());
lockeeEntry.setLockName(lock.getName()); entry.setStartTime(lock.getStartTime());
lockeeEntry.setStartTime(lock.getStartTime()); entry.setEndTime(lock.getUnlockTime());
lockeeEntry.setEndTime(lock.getUnlockTime()); entry.setDurationMinutes(durationMinutes);
lockeeEntry.setType(LockType.CARD); entry.addParticipant(lock.getLockee(), de.oaa.xxx.games.history.GameRole.LOCKEE);
lockeeEntry.setDurationMinutes(durationMinutes);
lockeeEntry.setRole("LOCKEE");
lockHistoryRepository.save(lockeeEntry);
// Eintrag für die Keyholderin
if (lock.getKeyholder() != null) { if (lock.getKeyholder() != null) {
LockHistoryEntity khEntry = new LockHistoryEntity(); entry.addParticipant(lock.getKeyholder(), de.oaa.xxx.games.history.GameRole.KEYHOLDER);
khEntry.setUserId(lock.getKeyholder());
khEntry.setLockedBy(lock.getLockee());
khEntry.setLockName(lock.getName());
khEntry.setStartTime(lock.getStartTime());
khEntry.setEndTime(lock.getUnlockTime());
khEntry.setType(LockType.CARD);
khEntry.setDurationMinutes(durationMinutes);
khEntry.setRole("KEYHOLDER");
lockHistoryRepository.save(khEntry);
} }
gameHistoryRepository.save(entry);
int minutes = (int) durationMinutes; int minutes = (int) durationMinutes;
userRepository.findById(lock.getLockee()).ifPresent(u -> { userRepository.findById(lock.getLockee()).ifPresent(u -> {

View File

@@ -3,10 +3,8 @@ package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity; import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository; import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task; import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.social.SseService; import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -25,8 +23,7 @@ public class TaskCardController {
private final CommunityTaskVoteRepository communityTaskVoteRepository; private final CommunityTaskVoteRepository communityTaskVoteRepository;
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository; private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
private final AssignedTaskRepository assignedTaskRepository; private final AssignedTaskRepository assignedTaskRepository;
private final MessageRepository messageRepository; private final SystemMessageService systemMessageService;
private final SseService sseService;
public TaskCardController(CardlockRepository cardlockRepository, public TaskCardController(CardlockRepository cardlockRepository,
UserRepository userRepository, UserRepository userRepository,
@@ -34,16 +31,14 @@ public class TaskCardController {
CommunityTaskVoteRepository communityTaskVoteRepository, CommunityTaskVoteRepository communityTaskVoteRepository,
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository, CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
AssignedTaskRepository assignedTaskRepository, AssignedTaskRepository assignedTaskRepository,
MessageRepository messageRepository, SystemMessageService systemMessageService) {
SseService sseService) {
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository; this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
this.communityTaskVoteRepository = communityTaskVoteRepository; this.communityTaskVoteRepository = communityTaskVoteRepository;
this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository; this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository;
this.assignedTaskRepository = assignedTaskRepository; this.assignedTaskRepository = assignedTaskRepository;
this.messageRepository = messageRepository; this.systemMessageService = systemMessageService;
this.sseService = sseService;
} }
// ── Keyholder: ausstehende Aufgaben-Karten-Entscheidungen ───────────────── // ── Keyholder: ausstehende Aufgaben-Karten-Entscheidungen ─────────────────
@@ -240,16 +235,6 @@ public class TaskCardController {
} }
private void sendMessage(UUID fromId, UUID toId, String text, String targetUrl) { private void sendMessage(UUID fromId, UUID toId, String text, String targetUrl) {
if (toId == null) return; systemMessageService.send(fromId, toId, text, targetUrl, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
MessageEntity msg = new MessageEntity();
msg.setMessageId(java.util.UUID.randomUUID());
msg.setSenderId(fromId);
msg.setReceiverId(toId);
msg.setText(text);
msg.setSystemMessage(true);
msg.setTargetUrl(targetUrl);
msg.setSentAt(java.time.LocalDateTime.now());
messageRepository.save(msg);
sseService.push(toId, "notification", Map.of("text", text));
} }
} }

View File

@@ -4,7 +4,6 @@ import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
@@ -17,9 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity; import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository; import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task; import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.social.SseService; import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
@Component @Component
public class TaskVoteScheduler { public class TaskVoteScheduler {
@@ -30,21 +27,18 @@ public class TaskVoteScheduler {
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository; private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final AssignedTaskRepository assignedTaskRepository; private final AssignedTaskRepository assignedTaskRepository;
private final MessageRepository messageRepository; private final SystemMessageService systemMessageService;
private final SseService sseService;
public TaskVoteScheduler(CommunityTaskVoteRepository communityTaskVoteRepository, public TaskVoteScheduler(CommunityTaskVoteRepository communityTaskVoteRepository,
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository, CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
CardlockRepository cardlockRepository, CardlockRepository cardlockRepository,
AssignedTaskRepository assignedTaskRepository, AssignedTaskRepository assignedTaskRepository,
MessageRepository messageRepository, SystemMessageService systemMessageService) {
SseService sseService) {
this.communityTaskVoteRepository = communityTaskVoteRepository; this.communityTaskVoteRepository = communityTaskVoteRepository;
this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository; this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository;
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.assignedTaskRepository = assignedTaskRepository; this.assignedTaskRepository = assignedTaskRepository;
this.messageRepository = messageRepository; this.systemMessageService = systemMessageService;
this.sseService = sseService;
} }
@Scheduled(fixedDelay = 60_000) @Scheduled(fixedDelay = 60_000)
@@ -117,16 +111,6 @@ public class TaskVoteScheduler {
} }
private void sendMessage(UUID toId, String text, String targetUrl) { private void sendMessage(UUID toId, String text, String targetUrl) {
if (toId == null) return; systemMessageService.send(toId, toId, text, targetUrl, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(toId); // System-Nachricht, kein echter Sender
msg.setReceiverId(toId);
msg.setText(text);
msg.setSystemMessage(true);
msg.setTargetUrl(targetUrl);
msg.setSentAt(LocalDateTime.now());
messageRepository.save(msg);
sseService.push(toId, "notification", Map.of("text", text));
} }
} }

View File

@@ -1,57 +0,0 @@
package de.oaa.xxx.games.chastity.history;
import de.oaa.xxx.user.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/lockhistory")
public class LockHistoryController {
private final UserRepository userRepository;
private final LockHistoryRepository lockHistoryRepository;
public LockHistoryController(UserRepository userRepository, LockHistoryRepository lockHistoryRepository) {
this.userRepository = userRepository;
this.lockHistoryRepository = lockHistoryRepository;
}
@GetMapping
public ResponseEntity<List<Map<String, Object>>> get(@RequestParam UUID userId, Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
var result = lockHistoryRepository.findByUserIdOrderByEndTimeDesc(userId).stream()
.map(e -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("role", e.getRole());
item.put("lockName", e.getLockName() != null ? e.getLockName() : "");
item.put("startTime", e.getStartTime().toString());
item.put("unlockTime", e.getEndTime().toString());
item.put("durationMinutes", e.getDurationMinutes());
if (e.getLockedBy() != null) {
userRepository.findById(e.getLockedBy()).ifPresent(u -> {
if ("LOCKEE".equals(e.getRole())) {
item.put("keyholderName", u.getName());
} else {
item.put("lockeeName", u.getName());
}
if (u.getProfilePicture() != null) {
item.put("partnerPic", u.getProfilePicture());
}
});
}
return item;
}).toList();
return ResponseEntity.ok(result);
}
}

View File

@@ -1,11 +0,0 @@
package de.oaa.xxx.games.chastity.history;
import java.time.LocalDateTime;
import java.util.UUID;
import de.oaa.xxx.games.chastity.LockType;
public record LockHistoryDTO (UUID historyId, UUID userId, LocalDateTime startTime, LocalDateTime endTime, LockType type, UUID lockedBy, String lockName, long durationMinutes, String role) {
}

View File

@@ -1,53 +0,0 @@
package de.oaa.xxx.games.chastity.history;
import java.time.LocalDateTime;
import java.util.UUID;
import de.oaa.xxx.games.chastity.LockType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "lock_history")
public class LockHistoryEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column
private UUID historyId;
@Column(nullable = false)
private UUID userId;
@Column(nullable = false)
private LocalDateTime startTime;
@Column(nullable = false)
private LocalDateTime endTime;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private LockType type;
@Column
private UUID lockedBy;
@Column
private String lockName;
@Column(nullable = false, columnDefinition = "BIGINT DEFAULT 0")
private long durationMinutes;
// LOCKEE oder KEYHOLDER
@Column(nullable = false, length = 20)
private String role;
public LockHistoryDTO toLockHistory() {
return new LockHistoryDTO(historyId, userId, startTime, endTime, type, lockedBy, lockName, durationMinutes, role);
}
}

View File

@@ -1,12 +0,0 @@
package de.oaa.xxx.games.chastity.history;
import java.util.List;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LockHistoryRepository extends JpaRepository<LockHistoryEntity, UUID> {
List<LockHistoryEntity> findByUserIdOrderByEndTimeDesc(UUID userId);
}

View File

@@ -0,0 +1,82 @@
package de.oaa.xxx.games.history;
import de.oaa.xxx.user.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/gamehistory")
public class GameHistoryController {
private final UserRepository userRepository;
private final GameHistoryRepository gameHistoryRepository;
public GameHistoryController(UserRepository userRepository, GameHistoryRepository gameHistoryRepository) {
this.userRepository = userRepository;
this.gameHistoryRepository = gameHistoryRepository;
}
@GetMapping
@Transactional(readOnly = true)
public ResponseEntity<List<Map<String, Object>>> get(@RequestParam UUID userId, Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
var result = gameHistoryRepository.findByParticipantUserId(userId).stream()
.map(e -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("historyId", e.getHistoryId());
item.put("gameType", e.getGameType());
item.put("gameName", e.getGameName() != null ? e.getGameName() : "");
item.put("lockName", e.getGameName() != null ? e.getGameName() : "");
item.put("startTime", e.getStartTime().toString());
item.put("unlockTime", e.getEndTime().toString());
item.put("durationMinutes", e.getDurationMinutes());
List<Map<String, Object>> participants = e.getParticipants().stream()
.map(p -> {
Map<String, Object> pm = new LinkedHashMap<>();
pm.put("userId", p.getUserId());
pm.put("role", p.getRole());
userRepository.findById(p.getUserId()).ifPresent(u -> {
pm.put("name", u.getName());
pm.put("picture", u.getProfilePicture());
});
return pm;
})
.toList();
item.put("participants", participants);
// Abwärtskompatible Felder für benutzer.html (wird später angepasst)
e.getParticipants().stream()
.filter(p -> p.getUserId().equals(userId))
.findFirst()
.ifPresent(own -> item.put("role", own.getRole().name()));
e.getParticipants().stream()
.filter(p -> !p.getUserId().equals(userId))
.findFirst()
.ifPresent(partner -> userRepository.findById(partner.getUserId()).ifPresent(u -> {
if (GameRole.LOCKEE == partner.getRole()) {
item.put("lockeeName", u.getName());
} else if (GameRole.KEYHOLDER == partner.getRole()) {
item.put("keyholderName", u.getName());
}
item.put("partnerPic", u.getProfilePicture());
}));
return item;
}).toList();
return ResponseEntity.ok(result);
}
}

View File

@@ -0,0 +1,17 @@
package de.oaa.xxx.games.history;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public record GameHistoryDTO(
UUID historyId,
GameType gameType,
LocalDateTime startTime,
LocalDateTime endTime,
String gameName,
long durationMinutes,
List<ParticipantDTO> participants
) {
public record ParticipantDTO(UUID userId, GameRole role) {}
}

View File

@@ -0,0 +1,49 @@
package de.oaa.xxx.games.history;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "game_history")
public class GameHistoryEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column
private UUID historyId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private GameType gameType;
@Column(nullable = false)
private LocalDateTime startTime;
@Column(nullable = false)
private LocalDateTime endTime;
@Column
private String gameName;
@Column(nullable = false, columnDefinition = "BIGINT DEFAULT 0")
private long durationMinutes;
@OneToMany(mappedBy = "history", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<GameHistoryParticipantEntity> participants = new ArrayList<>();
public void addParticipant(UUID userId, GameRole role) {
GameHistoryParticipantEntity p = new GameHistoryParticipantEntity();
p.setUserId(userId);
p.setRole(role);
p.setHistory(this);
participants.add(p);
}
}

View File

@@ -0,0 +1,30 @@
package de.oaa.xxx.games.history;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "game_history_participant")
public class GameHistoryParticipantEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column
private UUID participantId;
@Column(nullable = false)
private UUID userId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private GameRole role;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "history_id", nullable = false)
private GameHistoryEntity history;
}

View File

@@ -0,0 +1,8 @@
package de.oaa.xxx.games.history;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface GameHistoryParticipantRepository extends JpaRepository<GameHistoryParticipantEntity, UUID> {
}

View File

@@ -0,0 +1,17 @@
package de.oaa.xxx.games.history;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.UUID;
public interface GameHistoryRepository extends JpaRepository<GameHistoryEntity, UUID> {
@Query("SELECT DISTINCT h FROM GameHistoryEntity h JOIN h.participants p WHERE p.userId = :userId ORDER BY h.endTime DESC")
List<GameHistoryEntity> findByParticipantUserId(@Param("userId") UUID userId);
@Query("SELECT DISTINCT h FROM GameHistoryEntity h JOIN h.participants p WHERE p.userId = :userId AND h.gameType = :gameType ORDER BY h.endTime DESC")
List<GameHistoryEntity> findByParticipantUserIdAndGameType(@Param("userId") UUID userId, @Param("gameType") GameType gameType);
}

View File

@@ -0,0 +1,7 @@
package de.oaa.xxx.games.history;
public enum GameRole {
LOCKEE,
KEYHOLDER,
PLAYER
}

View File

@@ -0,0 +1,8 @@
package de.oaa.xxx.games.history;
public enum GameType {
CARDLOCK,
TIMELOCK,
BDSM,
VANILLA
}

View File

@@ -165,6 +165,56 @@ public class MailTemplateService {
); );
} }
public String buildNotificationMail(String name, String text, String targetUrl, String baseUrl) {
String actionButton = targetUrl != null
? """
<div style="text-align:center; margin:0 0 2rem 0;">
<a href="%s%s"
style="display:inline-block; padding:0.75rem 2.5rem; background:%s; color:#ffffff;
border-radius:6px; text-decoration:none; font-weight:600; font-size:1rem;">
Zur Anwendung
</a>
</div>
""".formatted(baseUrl, targetUrl, colorPrimary)
: "<div style=\"margin:0 0 2rem 0;\"></div>";
String settingsUrl = baseUrl + "/einstellungen.html#sec-benachrichtigungen";
return """
<!DOCTYPE html>
<html lang="de">
<body style="margin:0; padding:2rem; background:%s; font-family:'Segoe UI',Arial,sans-serif; color:%s;">
<div style="max-width:460px; margin:0 auto; background:%s; border:1px solid %s; border-radius:12px; padding:2.5rem; box-shadow:0 8px 32px rgba(0,0,0,0.5);">
<h1 style="color:%s; text-align:center; margin:0 0 1.5rem 0; font-size:1.6rem;">XXX The Game</h1>
<p style="color:%s; margin:0 0 0.75rem 0;">Hallo %s,</p>
<p style="color:%s; margin:0 0 2rem 0;">%s</p>
%s
<hr style="border:none; border-top:1px solid %s; margin:0 0 1.5rem 0;">
<p style="color:%s; font-size:0.85em; margin:0;">
Du erhältst diese E-Mail, weil du E-Mail-Benachrichtigungen für diese Kategorie aktiviert hast.
Du kannst deine Einstellungen jederzeit unter
<a href="%s" style="color:%s;">Einstellungen → Benachrichtigungen</a> anpassen.
</p>
</div>
</body>
</html>
""".formatted(
colorBg, colorText,
colorCard, colorSecondary,
colorPrimary,
colorText, name,
colorText, text,
actionButton,
colorSecondary,
colorMuted, settingsUrl, colorPrimary
);
}
public String buildActivationMail(String name, String activationLink, String activatePageUrl, String uuid) { public String buildActivationMail(String name, String activationLink, String activatePageUrl, String uuid) {
return """ return """
<!DOCTYPE html> <!DOCTYPE html>

View File

@@ -9,7 +9,6 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.user.Registration;
import de.oaa.xxx.user.UserController; import de.oaa.xxx.user.UserController;
@@ -29,12 +28,7 @@ public class ActivationController {
public ResponseEntity<Void> activate(@PathVariable String uuid) { public ResponseEntity<Void> activate(@PathVariable String uuid) {
RegistrationEntity registration = registrationRepository.findById(UUID.fromString(uuid)).orElse(null); RegistrationEntity registration = registrationRepository.findById(UUID.fromString(uuid)).orElse(null);
if (registration != null && !Boolean.TRUE.equals(registration.getActivated())) { if (registration != null && !Boolean.TRUE.equals(registration.getActivated())) {
Registration reg = new Registration(); ResponseEntity<Void> response = userController.userAnlegen(registration.toRegistration());
reg.setEmail(registration.getEmail());
reg.setName(registration.getName());
reg.setPasswordHash(registration.getPassword());
ResponseEntity<Void> response = userController.userAnlegen(reg);
if (response.getStatusCode().is2xxSuccessful()) { if (response.getStatusCode().is2xxSuccessful()) {
registration.setActivated(Boolean.TRUE); registration.setActivated(Boolean.TRUE);
registrationRepository.save(registration); registrationRepository.save(registration);

View File

@@ -3,6 +3,7 @@ package de.oaa.xxx.registration;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.time.LocalDate;
import java.util.UUID; import java.util.UUID;
@Getter @Getter
@@ -13,6 +14,7 @@ public class Registration {
private String name; private String name;
private String email; private String email;
private String passwordHash; private String passwordHash;
private LocalDate geburtsdatum;
@Override @Override
public String toString() { public String toString() {

View File

@@ -14,6 +14,9 @@ import de.oaa.xxx.mail.MailService;
import de.oaa.xxx.mail.MailTemplateService; import de.oaa.xxx.mail.MailTemplateService;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import java.time.LocalDate;
import java.time.Period;
@RestController @RestController
@RequestMapping("/registration") @RequestMapping("/registration")
public class RegistrationController { public class RegistrationController {
@@ -39,6 +42,11 @@ public class RegistrationController {
@PostMapping @PostMapping
public ResponseEntity<String> create(@RequestBody Registration registration) { public ResponseEntity<String> create(@RequestBody Registration registration) {
LOGGER.info("POST {}: {}", getClass().getName(), registration); LOGGER.info("POST {}: {}", getClass().getName(), registration);
if (registration.getGeburtsdatum() == null
|| Period.between(registration.getGeburtsdatum(), LocalDate.now()).getYears() < 18) {
LOGGER.warn("Registrierung abgelehnt Mindestalter nicht erreicht");
return ResponseEntity.status(422).build();
}
if (registrationRepository.findByEmail(registration.getEmail()).isPresent() if (registrationRepository.findByEmail(registration.getEmail()).isPresent()
|| userRepository.findByEmail(registration.getEmail()).isPresent()) { || userRepository.findByEmail(registration.getEmail()).isPresent()) {
LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail()); LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail());

View File

@@ -7,6 +7,7 @@ import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.time.LocalDate;
import java.util.UUID; import java.util.UUID;
@Getter @Getter
@@ -26,6 +27,8 @@ public class RegistrationEntity {
private String password; private String password;
@Column @Column
private Boolean activated; private Boolean activated;
@Column
private LocalDate geburtsdatum;
@Override @Override
public String toString() { public String toString() {
@@ -38,6 +41,7 @@ public class RegistrationEntity {
registration.setEmail(email); registration.setEmail(email);
registration.setName(name); registration.setName(name);
registration.setPasswordHash(password); registration.setPasswordHash(password);
registration.setGeburtsdatum(geburtsdatum);
return registration; return registration;
} }
@@ -48,6 +52,7 @@ public class RegistrationEntity {
entity.setActivated(Boolean.FALSE); entity.setActivated(Boolean.FALSE);
entity.setName(registration.getName()); entity.setName(registration.getName());
entity.setPassword(registration.getPasswordHash()); entity.setPassword(registration.getPasswordHash());
entity.setGeburtsdatum(registration.getGeburtsdatum());
return entity; return entity;
} }
} }

View File

@@ -1,12 +0,0 @@
package de.oaa.xxx.session.repository;
import de.oaa.xxx.session.entity.SessionEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface SessionRepository extends JpaRepository<SessionEntity, UUID> {
Optional<SessionEntity> findByUserId(UUID userId);
}

View File

@@ -6,6 +6,7 @@ import de.oaa.xxx.social.dto.MessageDto;
import de.oaa.xxx.social.dto.UserProfile; import de.oaa.xxx.social.dto.UserProfile;
import de.oaa.xxx.social.entity.FriendshipEntity; import de.oaa.xxx.social.entity.FriendshipEntity;
import de.oaa.xxx.social.entity.FriendshipEntity.Status; import de.oaa.xxx.social.entity.FriendshipEntity.Status;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.MessageEntity; import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.FriendshipRepository; import de.oaa.xxx.social.repository.FriendshipRepository;
import de.oaa.xxx.social.repository.MessageRepository; import de.oaa.xxx.social.repository.MessageRepository;
@@ -31,15 +32,18 @@ public class SocialController {
private final FriendshipRepository friendshipRepository; private final FriendshipRepository friendshipRepository;
private final MessageRepository messageRepository; private final MessageRepository messageRepository;
private final SseService sseService; private final SseService sseService;
private final SystemMessageService systemMessageService;
public SocialController(UserRepository userRepository, public SocialController(UserRepository userRepository,
FriendshipRepository friendshipRepository, FriendshipRepository friendshipRepository,
MessageRepository messageRepository, MessageRepository messageRepository,
SseService sseService) { SseService sseService,
SystemMessageService systemMessageService) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.friendshipRepository = friendshipRepository; this.friendshipRepository = friendshipRepository;
this.messageRepository = messageRepository; this.messageRepository = messageRepository;
this.sseService = sseService; this.sseService = sseService;
this.systemMessageService = systemMessageService;
} }
record FriendRequestBody(UUID receiverId) {} record FriendRequestBody(UUID receiverId) {}
@@ -94,6 +98,13 @@ public class SocialController {
f.setCreatedAt(LocalDateTime.now()); f.setCreatedAt(LocalDateTime.now());
friendshipRepository.save(f); friendshipRepository.save(f);
LOGGER.info("User {} hat Freundschaftsanfrage an User {} gesendet", myId, body.receiverId()); LOGGER.info("User {} hat Freundschaftsanfrage an User {} gesendet", myId, body.receiverId());
String senderName = meOpt.get().getName();
systemMessageService.send(myId, body.receiverId(),
senderName + " hat dir eine Freundschaftsanfrage gesendet.",
"/benutzer.html?userId=" + myId,
MessageCause.FRIENDREQUEST);
return ResponseEntity.status(201).build(); return ResponseEntity.status(201).build();
} }
@@ -299,22 +310,52 @@ public class SocialController {
// ── Helpers ── // ── Helpers ──
private UserProfile toUserProfileWithStatus(UserEntity user, UUID myId) { private UserProfile toUserProfileWithStatus(UserEntity user, UUID myId) {
boolean isOwn = user.getUserId().equals(myId);
String status = "NONE"; String status = "NONE";
var existing = friendshipRepository.findExisting(myId, user.getUserId()); if (!isOwn) {
if (existing.isPresent()) { var existing = friendshipRepository.findExisting(myId, user.getUserId());
FriendshipEntity f = existing.get(); if (existing.isPresent()) {
if (f.getStatus() == Status.ACCEPTED) { FriendshipEntity f = existing.get();
status = "FRIEND"; if (f.getStatus() == Status.ACCEPTED) {
} else if (f.getSenderId().equals(myId)) { status = "FRIEND";
status = "PENDING_SENT"; } else if (f.getSenderId().equals(myId)) {
} else { status = "PENDING_SENT";
status = "PENDING_RECEIVED"; } else {
status = "PENDING_RECEIVED";
}
} }
} }
return new UserProfile(user.getUserId(), user.getName(), user.getProfilePicture(), user.getProfilePictureHq(), boolean isFriend = isOwn || "FRIEND".equals(status);
status, user.getAlter(), user.getGroesse(), user.getGewicht(),
user.getGeschlecht(), user.getNeigung(), user.getBeziehungsstatus(), user.getBeschreibung(), // Grunddaten nur zurückgeben wenn berechtigt
user.getLockeeXp(), user.getKeyholderXp()); de.oaa.xxx.user.Sichtbarkeit svGd = user.getSichtbarkeitGrunddaten();
boolean showGrunddaten = isOwn || svGd == de.oaa.xxx.user.Sichtbarkeit.ALLE
|| (svGd == de.oaa.xxx.user.Sichtbarkeit.NUR_FREUNDE && isFriend);
// XP nur zurückgeben wenn berechtigt
de.oaa.xxx.user.Sichtbarkeit svXp = user.getSichtbarkeitXp();
boolean showXp = isOwn || svXp == de.oaa.xxx.user.Sichtbarkeit.ALLE
|| (svXp == de.oaa.xxx.user.Sichtbarkeit.NUR_FREUNDE && isFriend);
return new UserProfile(
user.getUserId(), user.getName(), user.getProfilePicture(), user.getProfilePictureHq(),
status,
showGrunddaten ? user.getAlter() : null,
showGrunddaten ? user.getGroesse() : null,
showGrunddaten ? user.getGewicht() : null,
showGrunddaten ? user.getGeschlecht() : null,
showGrunddaten ? user.getNeigung() : null,
showGrunddaten ? user.getBeziehungsstatus() : null,
showGrunddaten ? user.getBeschreibung() : null,
showXp ? user.getLockeeXp() : 0,
showXp ? user.getKeyholderXp() : 0,
user.getSichtbarkeitGrunddaten(),
user.getSichtbarkeitGalerie(),
user.getSichtbarkeitFreunde(),
user.getSichtbarkeitFeed(),
user.getSichtbarkeitPinnwand(),
user.getSichtbarkeitXp(),
user.getSichtbarkeitLockhistorie());
} }
private MessageDto toMessageDto(MessageEntity m) { private MessageDto toMessageDto(MessageEntity m) {

View File

@@ -0,0 +1,103 @@
package de.oaa.xxx.social;
import de.oaa.xxx.mail.Email;
import de.oaa.xxx.mail.MailService;
import de.oaa.xxx.mail.MailTemplateService;
import org.springframework.beans.factory.annotation.Value;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.social.repository.NotificationPreferenceRepository;
import de.oaa.xxx.user.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;
@Service
public class SystemMessageService {
private static final Logger LOGGER = LoggerFactory.getLogger(SystemMessageService.class);
private final MessageRepository messageRepository;
private final NotificationPreferenceRepository preferenceRepository;
private final UserRepository userRepository;
private final SseService sseService;
private final MailService mailService;
private final MailTemplateService mailTemplateService;
@Value("${app.base-url:http://localhost:8080}")
private String baseUrl;
public SystemMessageService(MessageRepository messageRepository,
NotificationPreferenceRepository preferenceRepository,
UserRepository userRepository,
SseService sseService,
MailService mailService,
MailTemplateService mailTemplateService) {
this.messageRepository = messageRepository;
this.preferenceRepository = preferenceRepository;
this.userRepository = userRepository;
this.sseService = sseService;
this.mailService = mailService;
this.mailTemplateService = mailTemplateService;
}
/**
* Sendet eine Systemnachricht unter Berücksichtigung der Benachrichtigungseinstellungen des Empfängers.
*/
public void send(UUID senderId, UUID receiverId, String text, String targetUrl, MessageCause cause) {
if (senderId == null || receiverId == null) return;
NotificationPreferenceEntity pref = preferenceRepository
.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();
if (sendInApp) {
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(senderId);
msg.setReceiverId(receiverId);
msg.setText(text);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
msg.setMessageCause(cause);
if (targetUrl != null) msg.setTargetUrl(targetUrl);
messageRepository.save(msg);
long unread = messageRepository.countByReceiverIdAndSystemMessageAndReadAtIsNull(receiverId, true);
sseService.push(receiverId, "NOTIFICATION", Map.of("unreadCount", unread, "text", text));
}
if (pref.isEmail()) {
userRepository.findById(receiverId).ifPresent(user -> {
try {
Email email = new Email();
email.setEmailAdresse(user.getEmail());
email.setTitel(causeTitel(cause));
email.setText(mailTemplateService.buildNotificationMail(user.getName(), text, targetUrl, baseUrl));
mailService.send(email);
} catch (Exception e) {
LOGGER.error("E-Mail-Benachrichtigung fehlgeschlagen für userId={}: {}", receiverId, e.getMessage());
}
});
}
}
private String causeTitel(MessageCause cause) {
return switch (cause) {
case INVITATION -> "XXX The Game Neue Einladung";
case GAME_STATE -> "XXX The Game Spielstatus-Änderung";
case EMERGENCY -> "XXX The Game ⚠️ Notfall";
case FRIENDREQUEST -> "XXX The Game Neue Freundschaftsanfrage";
};
}
}

View File

@@ -3,6 +3,7 @@ package de.oaa.xxx.social.dto;
import de.oaa.xxx.user.Beziehungsstatus; import de.oaa.xxx.user.Beziehungsstatus;
import de.oaa.xxx.user.Geschlecht; import de.oaa.xxx.user.Geschlecht;
import de.oaa.xxx.user.Neigung; import de.oaa.xxx.user.Neigung;
import de.oaa.xxx.user.Sichtbarkeit;
import java.util.UUID; import java.util.UUID;
@@ -20,10 +21,20 @@ public record UserProfile(
Beziehungsstatus beziehungsstatus, Beziehungsstatus beziehungsstatus,
String beschreibung, String beschreibung,
int lockeeXp, int lockeeXp,
int keyholderXp int keyholderXp,
// Datenschutz-Einstellungen
Sichtbarkeit sichtbarkeitGrunddaten,
Sichtbarkeit sichtbarkeitGalerie,
Sichtbarkeit sichtbarkeitFreunde,
Sichtbarkeit sichtbarkeitFeed,
Sichtbarkeit sichtbarkeitPinnwand,
Sichtbarkeit sichtbarkeitXp,
Sichtbarkeit sichtbarkeitLockhistorie
) { ) {
/** Compact constructor for contexts where profile details are not needed (friend list etc.) */ /** Compact constructor for contexts where profile details are not needed (friend list etc.) */
public UserProfile(UUID userId, String name, String profilePicture, String profilePictureHq, String friendStatus) { public UserProfile(UUID userId, String name, String profilePicture, String profilePictureHq, String friendStatus) {
this(userId, name, profilePicture, profilePictureHq, friendStatus, null, null, null, null, null, null, null, 0, 0); this(userId, name, profilePicture, profilePictureHq, friendStatus,
null, null, null, null, null, null, null, 0, 0,
null, null, null, null, null, null, null);
} }
} }

View File

@@ -0,0 +1,8 @@
package de.oaa.xxx.social.entity;
public enum MessageCause {
INVITATION,
GAME_STATE,
EMERGENCY,
FRIENDREQUEST
}

View File

@@ -35,6 +35,10 @@ public class MessageEntity {
@Column(nullable = false) @Column(nullable = false)
private boolean systemMessage = false; private boolean systemMessage = false;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private MessageCause messageCause;
@Column(length = 500) @Column(length = 500)
private String targetUrl; private String targetUrl;
} }

View File

@@ -0,0 +1,40 @@
package de.oaa.xxx.social.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "notification_preference", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "cause"}))
public class NotificationPreferenceEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "user_id", nullable = false)
private UUID userId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private MessageCause cause;
@Column(nullable = false)
private boolean inApp = true;
@Column(nullable = false)
private boolean email = false;
/** Erzeugt eine nicht persistierte Standardpräferenz für unbekannte/neue Causes. */
public static NotificationPreferenceEntity defaultFor(UUID userId, MessageCause cause) {
NotificationPreferenceEntity p = new NotificationPreferenceEntity();
p.setUserId(userId);
p.setCause(cause);
// inApp=true und email=false sind bereits die Java-Felddefaults
return p;
}
}

View File

@@ -0,0 +1,16 @@
package de.oaa.xxx.social.repository;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface NotificationPreferenceRepository extends JpaRepository<NotificationPreferenceEntity, UUID> {
List<NotificationPreferenceEntity> findByUserId(UUID userId);
Optional<NotificationPreferenceEntity> findByUserIdAndCause(UUID userId, MessageCause cause);
}

View File

@@ -1,21 +0,0 @@
package de.oaa.xxx.user;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
public class Registration {
private UUID id;
private String name;
private String email;
private String passwordHash;
@Override
public String toString() {
return "Registration [id=" + id + ", name=" + name + ", email=" + email + "]";
}
}

View File

@@ -0,0 +1,7 @@
package de.oaa.xxx.user;
public enum Sichtbarkeit {
ALLE,
NUR_FREUNDE,
NUR_ICH
}

View File

@@ -3,6 +3,8 @@ package de.oaa.xxx.user;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.time.LocalDate;
import java.time.Period;
import java.util.UUID; import java.util.UUID;
@Getter @Getter
@@ -14,7 +16,7 @@ public class User {
private String email; private String email;
private String password; private String password;
private String profilePicture; private String profilePicture;
private Integer alter; private LocalDate geburtsdatum;
private Integer groesse; private Integer groesse;
private Integer gewicht; private Integer gewicht;
private Geschlecht geschlecht; private Geschlecht geschlecht;
@@ -22,6 +24,10 @@ public class User {
private Beziehungsstatus beziehungsstatus; private Beziehungsstatus beziehungsstatus;
private String beschreibung; private String beschreibung;
public Integer getAlter() {
return geburtsdatum != null ? Period.between(geburtsdatum, LocalDate.now()).getYears() : null;
}
@Override @Override
public String toString() { public String toString() {
return "User[userId=" + userId + ", name=" + name + ", email=" + email + "]"; return "User[userId=" + userId + ", name=" + name + ", email=" + email + "]";

View File

@@ -1,8 +1,13 @@
package de.oaa.xxx.user; package de.oaa.xxx.user;
import java.security.Principal; import java.security.Principal;
import java.time.LocalDate;
import java.time.Period;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -10,6 +15,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@@ -17,6 +23,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.aufgaben.repository.AufgabeRepository; import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity;
import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository; import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.FavoritRepository; import de.oaa.xxx.aufgaben.repository.FavoritRepository;
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository; import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
@@ -24,19 +32,23 @@ import de.oaa.xxx.aufgaben.repository.SperreRepository;
import de.oaa.xxx.aufgaben.repository.StrafeRepository; import de.oaa.xxx.aufgaben.repository.StrafeRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository; import de.oaa.xxx.aufgaben.repository.ToyRepository;
import de.oaa.xxx.emailchange.EmailChangeRepository; import de.oaa.xxx.emailchange.EmailChangeRepository;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.passwordreset.PasswordResetRepository; import de.oaa.xxx.passwordreset.PasswordResetRepository;
import de.oaa.xxx.registration.Registration;
import de.oaa.xxx.registration.RegistrationRepository; import de.oaa.xxx.registration.RegistrationRepository;
import de.oaa.xxx.session.entity.AktiveSperreEntity; import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.session.entity.MitspielerEntity; import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository; import de.oaa.xxx.social.repository.KommentarLikeRepository;
import de.oaa.xxx.session.repository.MitspielerRepository; import de.oaa.xxx.social.repository.KommentarRepository;
import de.oaa.xxx.session.repository.SessionRepository; import de.oaa.xxx.social.repository.NotificationPreferenceRepository;
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
import de.oaa.xxx.social.repository.ProfileImageRepository;
import de.oaa.xxx.social.repository.PinnwandEintragRepository; import de.oaa.xxx.social.repository.PinnwandEintragRepository;
import de.oaa.xxx.social.repository.PinnwandLikeRepository; import de.oaa.xxx.social.repository.PinnwandLikeRepository;
import de.oaa.xxx.social.repository.KommentarRepository; import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
import de.oaa.xxx.social.repository.KommentarLikeRepository; import de.oaa.xxx.social.repository.ProfileImageRepository;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
@RestController @RestController
@@ -54,7 +66,7 @@ public class UserController {
private final ToyRepository toyRepository; private final ToyRepository toyRepository;
private final FavoritRepository favoritRepository; private final FavoritRepository favoritRepository;
private final GruppenAboRepository gruppenAboRepository; private final GruppenAboRepository gruppenAboRepository;
private final SessionRepository sessionRepository; private final BdsmGameRepository sessionRepository;
private final AktiveSperreRepository aktiveSperreRepository; private final AktiveSperreRepository aktiveSperreRepository;
private final MitspielerRepository mitspielerRepository; private final MitspielerRepository mitspielerRepository;
private final EmailChangeRepository emailChangeRepository; private final EmailChangeRepository emailChangeRepository;
@@ -65,6 +77,8 @@ public class UserController {
private final PinnwandLikeRepository pinnwandLikeRepository; private final PinnwandLikeRepository pinnwandLikeRepository;
private final KommentarRepository kommentarRepository; private final KommentarRepository kommentarRepository;
private final KommentarLikeRepository kommentarLikeRepository; private final KommentarLikeRepository kommentarLikeRepository;
private final NotificationPreferenceRepository notificationPreferenceRepository;
private final BdsmDefaultsRepository bdsmDefaultsRepository;
public UserController(UserRepository userRepository, public UserController(UserRepository userRepository,
RegistrationRepository registrationRepository, RegistrationRepository registrationRepository,
@@ -75,7 +89,7 @@ public class UserController {
ToyRepository toyRepository, ToyRepository toyRepository,
FavoritRepository favoritRepository, FavoritRepository favoritRepository,
GruppenAboRepository gruppenAboRepository, GruppenAboRepository gruppenAboRepository,
SessionRepository sessionRepository, BdsmGameRepository sessionRepository,
AktiveSperreRepository aktiveSperreRepository, AktiveSperreRepository aktiveSperreRepository,
MitspielerRepository mitspielerRepository, MitspielerRepository mitspielerRepository,
EmailChangeRepository emailChangeRepository, EmailChangeRepository emailChangeRepository,
@@ -85,7 +99,9 @@ public class UserController {
PinnwandEintragRepository pinnwandEintragRepository, PinnwandEintragRepository pinnwandEintragRepository,
PinnwandLikeRepository pinnwandLikeRepository, PinnwandLikeRepository pinnwandLikeRepository,
KommentarRepository kommentarRepository, KommentarRepository kommentarRepository,
KommentarLikeRepository kommentarLikeRepository) { KommentarLikeRepository kommentarLikeRepository,
NotificationPreferenceRepository notificationPreferenceRepository,
BdsmDefaultsRepository bdsmDefaultsRepository) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.registrationRepository = registrationRepository; this.registrationRepository = registrationRepository;
this.aufgabenGruppeRepository = aufgabenGruppeRepository; this.aufgabenGruppeRepository = aufgabenGruppeRepository;
@@ -106,12 +122,23 @@ public class UserController {
this.pinnwandLikeRepository = pinnwandLikeRepository; this.pinnwandLikeRepository = pinnwandLikeRepository;
this.kommentarRepository = kommentarRepository; this.kommentarRepository = kommentarRepository;
this.kommentarLikeRepository = kommentarLikeRepository; this.kommentarLikeRepository = kommentarLikeRepository;
this.notificationPreferenceRepository = notificationPreferenceRepository;
this.bdsmDefaultsRepository = bdsmDefaultsRepository;
} }
record ProfilePictureRequest(String picture, String pictureHq) {} record ProfilePictureRequest(String picture, String pictureHq) {}
record NameChangeRequest(String name) {} record NameChangeRequest(String name) {}
record ProfileRequest(Integer alter, Integer groesse, Integer gewicht, record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {}
record ProfileRequest(Integer groesse, Integer gewicht,
Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {} Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {}
record PrivacyRequest(
Sichtbarkeit sichtbarkeitGrunddaten,
Sichtbarkeit sichtbarkeitGalerie,
Sichtbarkeit sichtbarkeitFreunde,
Sichtbarkeit sichtbarkeitFeed,
Sichtbarkeit sichtbarkeitPinnwand,
Sichtbarkeit sichtbarkeitXp,
Sichtbarkeit sichtbarkeitLockhistorie) {}
@PutMapping("/me/picture") @PutMapping("/me/picture")
public ResponseEntity<Void> updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) { public ResponseEntity<Void> updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) {
@@ -132,7 +159,6 @@ public class UserController {
if (request.beschreibung() != null && request.beschreibung().length() > 600) { if (request.beschreibung() != null && request.beschreibung().length() > 600) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
user.setAlter(request.alter());
user.setGroesse(request.groesse()); user.setGroesse(request.groesse());
user.setGewicht(request.gewicht()); user.setGewicht(request.gewicht());
user.setGeschlecht(request.geschlecht()); user.setGeschlecht(request.geschlecht());
@@ -144,6 +170,126 @@ public class UserController {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@PutMapping("/me/privacy")
public ResponseEntity<Void> updatePrivacy(@RequestBody PrivacyRequest request, Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
var user = userOpt.get();
if (request.sichtbarkeitGrunddaten() != null) user.setSichtbarkeitGrunddaten(request.sichtbarkeitGrunddaten());
if (request.sichtbarkeitGalerie() != null) user.setSichtbarkeitGalerie(request.sichtbarkeitGalerie());
if (request.sichtbarkeitFreunde() != null) user.setSichtbarkeitFreunde(request.sichtbarkeitFreunde());
if (request.sichtbarkeitFeed() != null) user.setSichtbarkeitFeed(request.sichtbarkeitFeed());
if (request.sichtbarkeitPinnwand() != null) user.setSichtbarkeitPinnwand(request.sichtbarkeitPinnwand());
if (request.sichtbarkeitXp() != null) user.setSichtbarkeitXp(request.sichtbarkeitXp());
if (request.sichtbarkeitLockhistorie()!= null) user.setSichtbarkeitLockhistorie(request.sichtbarkeitLockhistorie());
userRepository.save(user);
LOGGER.info("User {} hat Datenschutz-Einstellungen aktualisiert", user.getUserId());
return ResponseEntity.ok().build();
}
record NotificationPreferenceRequest(boolean inApp, boolean email) {}
@GetMapping("/me/notifications")
public ResponseEntity<Map<String, Object>> getNotifications(Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID userId = userOpt.get().getUserId();
Map<String, NotificationPreferenceEntity> byKey = notificationPreferenceRepository.findByUserId(userId)
.stream().collect(Collectors.toMap(p -> p.getCause().name(), p -> p));
Map<String, Object> result = new LinkedHashMap<>();
for (MessageCause cause : MessageCause.values()) {
NotificationPreferenceEntity pref = byKey.getOrDefault(
cause.name(), NotificationPreferenceEntity.defaultFor(userId, cause));
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("inApp", pref.isInApp());
entry.put("email", pref.isEmail());
result.put(cause.name(), entry);
}
return ResponseEntity.ok(result);
}
@PutMapping("/me/notifications")
public ResponseEntity<Void> updateNotifications(@RequestBody Map<String, NotificationPreferenceRequest> request, Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID userId = userOpt.get().getUserId();
for (var entry : request.entrySet()) {
MessageCause cause;
try {
cause = MessageCause.valueOf(entry.getKey());
} catch (IllegalArgumentException e) {
continue;
}
NotificationPreferenceEntity pref = notificationPreferenceRepository
.findByUserIdAndCause(userId, cause)
.orElseGet(() -> {
NotificationPreferenceEntity n = new NotificationPreferenceEntity();
n.setUserId(userId);
n.setCause(cause);
return n;
});
pref.setInApp(entry.getValue().inApp());
pref.setEmail(entry.getValue().email());
notificationPreferenceRepository.save(pref);
}
return ResponseEntity.ok().build();
}
record BdsmDefaultsRequest(List<String> spieltMit, List<String> rollen, List<String> werkzeuge) {}
@GetMapping("/me/bdsm-defaults")
public ResponseEntity<Map<String, Object>> getBdsmDefaults(Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID userId = userOpt.get().getUserId();
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
.orElse(new BdsmDefaultsEntity());
Map<String, Object> result = new java.util.LinkedHashMap<>();
result.put("spieltMit", splitOrEmpty(d.getSpieltMit()));
result.put("rollen", splitOrEmpty(d.getRollen()));
result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge()));
return ResponseEntity.ok(result);
}
@PutMapping("/me/bdsm-defaults")
public ResponseEntity<Void> updateBdsmDefaults(@RequestBody BdsmDefaultsRequest request, Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID userId = userOpt.get().getUserId();
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
.orElseGet(() -> { BdsmDefaultsEntity n = new BdsmDefaultsEntity(); n.setUserId(userId); return n; });
d.setSpieltMit(request.spieltMit() == null ? "" : String.join(",", request.spieltMit()));
d.setRollen(request.rollen() == null ? "" : String.join(",", request.rollen()));
d.setWerkzeuge(request.werkzeuge() == null ? "" : String.join(",", request.werkzeuge()));
bdsmDefaultsRepository.save(d);
return ResponseEntity.ok().build();
}
private static List<String> splitOrEmpty(String s) {
if (s == null || s.isBlank()) return List.of();
return List.of(s.split(","));
}
@PutMapping("/me/geburtsdatum")
public ResponseEntity<Void> updateGeburtsdatum(@RequestBody GeburtsdatumChangeRequest request, Principal principal) {
if (request.geburtsdatum() == null
|| Period.between(request.geburtsdatum(), LocalDate.now()).getYears() < 18) {
return ResponseEntity.status(422).build();
}
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
var user = userOpt.get();
user.setGeburtsdatum(request.geburtsdatum());
userRepository.save(user);
LOGGER.info("User {} hat Geburtsdatum aktualisiert", user.getUserId());
return ResponseEntity.ok().build();
}
@PutMapping("/me/name") @PutMapping("/me/name")
public ResponseEntity<Void> updateName(@RequestBody NameChangeRequest request, Principal principal) { public ResponseEntity<Void> updateName(@RequestBody NameChangeRequest request, Principal principal) {
String newName = request.name(); String newName = request.name();
@@ -264,7 +410,14 @@ public class UserController {
entity.setEmail(registration.getEmail()); entity.setEmail(registration.getEmail());
entity.setName(registration.getName()); entity.setName(registration.getName());
entity.setPassword(registration.getPasswordHash()); entity.setPassword(registration.getPasswordHash());
entity.setGeburtsdatum(registration.getGeburtsdatum());
userRepository.save(entity); userRepository.save(entity);
for (MessageCause cause : MessageCause.values()) {
notificationPreferenceRepository.save(
NotificationPreferenceEntity.defaultFor(entity.getUserId(), cause));
}
return ResponseEntity.status(201).build(); return ResponseEntity.status(201).build();
} catch (Exception exception) { } catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception); LOGGER.error(exception.getMessage(), exception);

View File

@@ -4,6 +4,8 @@ import jakarta.persistence.*;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.time.LocalDate;
import java.time.Period;
import java.util.UUID; import java.util.UUID;
@Getter @Getter
@@ -28,8 +30,8 @@ public class UserEntity {
@Column(columnDefinition = "MEDIUMTEXT") @Column(columnDefinition = "MEDIUMTEXT")
private String profilePictureHq; private String profilePictureHq;
@Column(name = "benutzer_alter") @Column
private Integer alter; private LocalDate geburtsdatum;
@Column @Column
private Integer groesse; private Integer groesse;
@@ -58,6 +60,39 @@ public class UserEntity {
@Column(nullable = false, columnDefinition = "INT DEFAULT 0") @Column(nullable = false, columnDefinition = "INT DEFAULT 0")
private int keyholderXp; private int keyholderXp;
// ── Datenschutz / Sichtbarkeit ──
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitGrunddaten = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitGalerie = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitFreunde = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitFeed = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitPinnwand = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitXp = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitLockhistorie = Sichtbarkeit.ALLE;
public Integer getAlter() {
return geburtsdatum != null ? Period.between(geburtsdatum, LocalDate.now()).getYears() : null;
}
@Override @Override
public String toString() { public String toString() {
return "UserEntity[userId=" + userId + ", name=" + name + ", email=" + email + "]"; return "UserEntity[userId=" + userId + ", name=" + name + ", email=" + email + "]";
@@ -69,7 +104,7 @@ public class UserEntity {
user.setName(name); user.setName(name);
user.setUserId(userId); user.setUserId(userId);
user.setProfilePicture(profilePicture); user.setProfilePicture(profilePicture);
user.setAlter(alter); user.setGeburtsdatum(geburtsdatum);
user.setGroesse(groesse); user.setGroesse(groesse);
user.setGewicht(gewicht); user.setGewicht(gewicht);
user.setGeschlecht(geschlecht); user.setGeschlecht(geschlecht);

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aufgaben XXX The Game</title> <title>Aufgaben XXX The Game</title>
@@ -608,44 +608,47 @@
resetSelection(); resetSelection();
document.getElementById('userLoading').style.display = 'block'; document.getElementById('userLoading').style.display = 'block';
fetch(`/gruppe/list/user?page=${userPage}&size=${PAGE_SIZE}`) fetch(`/gruppe/list/user?page=${userPage}&size=${PAGE_SIZE}`)
.then(r => r.json()) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(data => { .then(data => {
console.log('[aufgaben] user gruppen:', data);
userTotalPages = data.totalPages || 1; userTotalPages = data.totalPages || 1;
renderGruppen('userList', data.content, 'user'); try { renderGruppen('userList', data.content, 'user'); } catch(e) { console.error('[aufgaben] renderGruppen user Fehler:', e); throw e; }
updatePaging('userPaging', 'userPrev', 'userNext', 'userPageInfo', userPage, userTotalPages); updatePaging('userPaging', 'userPrev', 'userNext', 'userPageInfo', userPage, userTotalPages);
document.getElementById('userLoading').style.display = 'none'; document.getElementById('userLoading').style.display = 'none';
reapplyPendingExpand(); reapplyPendingExpand();
}) })
.catch(() => { document.getElementById('userLoading').textContent = 'Fehler beim Laden.'; }); .catch(err => { console.error('[aufgaben] Fehler user gruppen:', err); document.getElementById('userLoading').textContent = 'Fehler beim Laden: ' + err.message; });
} }
function loadSystemGruppen() { function loadSystemGruppen() {
resetSelection(); resetSelection();
document.getElementById('systemLoading').style.display = 'block'; document.getElementById('systemLoading').style.display = 'block';
fetch(`/gruppe/list/system?page=${systemPage}&size=${PAGE_SIZE}`) fetch(`/gruppe/list/system?page=${systemPage}&size=${PAGE_SIZE}`)
.then(r => r.json()) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(data => { .then(data => {
console.log('[aufgaben] system gruppen:', data);
systemTotalPages = data.totalPages || 1; systemTotalPages = data.totalPages || 1;
renderGruppen('systemList', data.content, 'system'); try { renderGruppen('systemList', data.content, 'system'); } catch(e) { console.error('[aufgaben] renderGruppen system Fehler:', e); throw e; }
updatePaging('systemPaging', 'systemPrev', 'systemNext', 'systemPageInfo', systemPage, systemTotalPages); updatePaging('systemPaging', 'systemPrev', 'systemNext', 'systemPageInfo', systemPage, systemTotalPages);
document.getElementById('systemLoading').style.display = 'none'; document.getElementById('systemLoading').style.display = 'none';
reapplyPendingExpand(); reapplyPendingExpand();
}) })
.catch(() => { document.getElementById('systemLoading').textContent = 'Fehler beim Laden.'; }); .catch(err => { console.error('[aufgaben] Fehler system gruppen:', err); document.getElementById('systemLoading').textContent = 'Fehler beim Laden: ' + err.message; });
} }
function loadAboGruppen() { function loadAboGruppen() {
document.getElementById('aboLoading').style.display = 'block'; document.getElementById('aboLoading').style.display = 'block';
fetch(`/abo/list?page=${aboPage}&size=${PAGE_SIZE}`) fetch(`/abo/list?page=${aboPage}&size=${PAGE_SIZE}`)
.then(r => r.json()) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(data => { .then(data => {
console.log('[aufgaben] abo gruppen:', data);
aboTotalPages = data.totalPages || 1; aboTotalPages = data.totalPages || 1;
renderGruppen('aboList', data.content, 'abo'); renderGruppen('aboList', data.content, 'abo');
updatePaging('aboPaging', 'aboPrev', 'aboNext', 'aboPageInfo', aboPage, aboTotalPages); updatePaging('aboPaging', 'aboPrev', 'aboNext', 'aboPageInfo', aboPage, aboTotalPages);
document.getElementById('aboLoading').style.display = 'none'; document.getElementById('aboLoading').style.display = 'none';
reapplyPendingExpand(); reapplyPendingExpand();
}) })
.catch(() => { document.getElementById('aboLoading').textContent = 'Fehler beim Laden.'; }); .catch(err => { console.error('[aufgaben] Fehler abo gruppen:', err); document.getElementById('aboLoading').textContent = 'Fehler beim Laden: ' + err.message; });
} }
function reapplyPendingExpand() { function reapplyPendingExpand() {

View File

@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Einladung XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.invite-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 2rem;
text-align: center;
max-width: 420px;
margin: 0 auto;
}
.invite-icon { font-size: 2.5rem; margin-bottom: 1rem; }
.invite-title { font-size: 1.2rem; font-weight: 700; margin-bottom: 0.5rem; }
.invite-sub { font-size: 0.9rem; color: var(--color-muted); margin-bottom: 2rem; line-height: 1.6; }
.invite-actions { display: flex; flex-direction: column; gap: 0.75rem; }
.invite-actions button { width: 100%; padding: 0.85rem; }
.decline-btn {
background: transparent;
border: none;
color: var(--color-muted);
font-size: 0.82rem;
cursor: pointer;
text-decoration: underline;
padding: 0.25rem;
margin-top: 0.5rem;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<div id="loading" style="text-align:center;color:var(--color-muted);padding:3rem 0;">Einladung wird geladen…</div>
<div class="invite-card" id="card" style="display:none;">
<div class="invite-icon">⛓️</div>
<div class="invite-title" id="title"></div>
<div class="invite-sub" id="sub"></div>
<div class="message" id="message" style="display:none;margin-bottom:1rem;"></div>
<div class="invite-actions" id="actions"></div>
</div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
const params = new URLSearchParams(location.search);
const einladungId = params.get('id');
if (!einladungId) window.location.replace('/userhome.html');
let einladung = null;
async function laden() {
try {
const res = await fetch(`/bdsm/einladung/${einladungId}`);
if (!res.ok) { zeigeFehler('Einladung nicht gefunden.'); return; }
einladung = await res.json();
document.getElementById('loading').style.display = 'none';
document.getElementById('card').style.display = '';
if (einladung.status === 'ACCEPTED_OWN' || einladung.status === 'ACCEPTED_HOST') {
zeigeBestaetigt();
return;
}
if (einladung.status === 'DECLINED' || einladung.status === 'CANCELLED') {
zeigeFehler('Diese Einladung ist nicht mehr gültig.');
return;
}
document.getElementById('title').textContent = `${einladung.inviterName || 'Jemand'} lädt dich ein`;
document.getElementById('sub').textContent = 'Du wurdest zu einem BDSM Game eingeladen. Wie möchtest du mitspielen?';
const actions = document.getElementById('actions');
actions.innerHTML = `
<button onclick="antworten(true, 'OWN_DEVICE')">Am eigenen Gerät mitspielen</button>
<button class="secondary" onclick="antworten(true, 'HOST_DEVICE')">Am Gerät von ${einladung.inviterName || 'der einladenden Person'}</button>
<button class="decline-btn" onclick="antworten(false, null)">Einladung ablehnen</button>`;
} catch (e) {
zeigeFehler('Fehler beim Laden der Einladung.');
}
}
async function antworten(accepted, mode) {
document.getElementById('actions').innerHTML = '<div style="color:var(--color-muted);font-size:0.9rem;">Wird gespeichert…</div>';
try {
const res = await fetch(`/bdsm/einladung/${einladungId}/antwort`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accepted, mode }),
});
if (!res.ok) throw new Error();
if (!accepted) {
document.getElementById('title').textContent = 'Einladung abgelehnt';
document.getElementById('sub').textContent = 'Du hast die Einladung abgelehnt.';
document.getElementById('actions').innerHTML = '<button onclick="window.location.href=\'/userhome.html\'">Zur Startseite</button>';
} else if (mode === 'OWN_DEVICE') {
window.location.replace(`/bdsmwarten.html?id=${einladungId}`);
} else {
zeigeBestaetigt();
}
} catch (_) {
document.getElementById('actions').innerHTML = '';
zeigeFehler('Fehler beim Speichern der Antwort.');
}
}
function zeigeBestaetigt() {
document.getElementById('title').textContent = 'Einladung angenommen';
document.getElementById('sub').textContent = 'Du spielst am Gerät der einladenden Person mit. Das Spiel wird dort von ihr gestartet.';
document.getElementById('actions').innerHTML = '<button onclick="window.location.href=\'/userhome.html\'">Zur Startseite</button>';
}
function zeigeFehler(text) {
document.getElementById('loading').style.display = 'none';
document.getElementById('card').style.display = '';
document.getElementById('title').textContent = 'Hinweis';
document.getElementById('sub').textContent = text;
document.getElementById('actions').innerHTML = '<button onclick="window.location.href=\'/userhome.html\'">Zur Startseite</button>';
}
laden();
</script>
</body>
</html>

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Neue Session XXX The Game</title> <title>BDSM Game Neue Session XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
.session-setup { max-width: 540px; } .session-setup { }
.setup-section { margin-bottom: 2.5rem; } .setup-section { margin-bottom: 2.5rem; }
.setup-section h2 { .setup-section h2 {
@@ -180,7 +180,7 @@
aufgabenProLevel: parseInt(document.getElementById('sldAufgaben').value), aufgabenProLevel: parseInt(document.getElementById('sldAufgaben').value),
zeitfaktorZeitstrafen: parseInt(document.getElementById('sldZeit').value) / 10, zeitfaktorZeitstrafen: parseInt(document.getElementById('sldZeit').value) / 10,
})); }));
window.location.href = '/sessionbdsmplayers.html'; window.location.href = '/bdsmplayers.html';
} }
function showMessage(text, type) { function showMessage(text, type) {
@@ -224,7 +224,7 @@
function sessionFortfahren(sid) { function sessionFortfahren(sid) {
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k)); BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
sessionStorage.setItem('bdsm-session-id', sid); sessionStorage.setItem('bdsm-session-id', sid);
window.location.href = '/sessionbdsmingame.html'; window.location.href = '/bdsmingame.html';
} }
function sessionBeendenFragen(sid) { function sessionBeendenFragen(sid) {
@@ -241,7 +241,7 @@
async function sessionLoeschen(sid) { async function sessionLoeschen(sid) {
versteckeModal(); versteckeModal();
try { try {
await fetch('/session', { await fetch('/bdsm', {
method: 'DELETE', method: 'DELETE',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: sid }), body: JSON.stringify({ sessionId: sid }),
@@ -256,7 +256,7 @@
if (!meRes.ok) return; if (!meRes.ok) return;
const user = await meRes.json(); const user = await meRes.json();
const sessionRes = await fetch(`/session?userId=${user.userId}`); const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
if (sessionRes.status === 204) return; if (sessionRes.status === 204) return;
if (!sessionRes.ok) return; if (!sessionRes.ok) return;

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Im Spiel XXX The Game</title> <title>BDSM Game Im Spiel XXX The Game</title>
@@ -236,7 +236,12 @@
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup') || 'null'); const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup') || 'null');
const toys = JSON.parse(sessionStorage.getItem('bdsm-session-toys') || '[]'); const toys = JSON.parse(sessionStorage.getItem('bdsm-session-toys') || '[]');
const sessionId = sessionStorage.getItem('bdsm-session-id'); const sessionId = sessionStorage.getItem('bdsm-session-id');
if (!sessionId) window.location.replace('/sessionbdsm.html'); if (!sessionId) window.location.replace('/bdsm.html');
// Multi-Device: bin ich Gast?
const isGuest = sessionStorage.getItem('bdsm-is-guest') === 'true';
const myMitspielerId = sessionStorage.getItem('bdsm-guest-mitspieler-id') || null;
let guestPollInterval = null;
// ── Modal ── // ── Modal ──
function zeigeModal(title, text, actions) { function zeigeModal(title, text, actions) {
@@ -283,6 +288,7 @@
function clearTimer() { function clearTimer() {
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
stopHostPoll();
} }
function zeigeTaskFehler(text) { function zeigeTaskFehler(text) {
@@ -310,7 +316,7 @@
card.innerHTML = 'Aufgabe wird geladen…'; card.innerHTML = 'Aufgabe wird geladen…';
try { try {
const res = await fetch(`/session/${sessionId}/aufgaben/next`); const res = await fetch(`/bdsm/${sessionId}/aufgaben/next`);
if (res.status === 204) { zeigeFinaleDialog(); return; } if (res.status === 204) { zeigeFinaleDialog(); return; }
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
currentTask = await res.json(); currentTask = await res.json();
@@ -326,9 +332,189 @@
} }
} }
// ── Aktive Aufgabe persistieren ──
async function saveAktiveAufgabe(task, timerStartedAt) {
try {
await fetch(`/bdsm/${sessionId}/active-task`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ taskJson: JSON.stringify(task), timerStartedAt }),
});
} catch (_) { /* ignorieren */ }
}
async function clearAktiveAufgabe() {
try {
await fetch(`/bdsm/${sessionId}/active-task`, { method: 'DELETE' });
} catch (_) { /* ignorieren */ }
}
async function checkAktiveAufgabe() {
try {
const res = await fetch(`/bdsm/${sessionId}/active-task`);
if (res.status === 204 || !res.ok) { ladeAufgabe(); return; }
const data = await res.json();
currentTask = JSON.parse(data.taskJson);
if (currentTask.level) {
document.getElementById('levelImg').src = `/img/lvl${currentTask.level}.png`;
document.getElementById('levelDisplay').style.display = '';
}
if (data.elapsedSeconds !== null && data.elapsedSeconds !== undefined) {
const remaining = currentTask.timer - data.elapsedSeconds;
if (remaining <= 0) {
await clearAktiveAufgabe();
aufgabeAbgeschlossen();
} else {
restoreTimer(Math.floor(remaining));
}
} else {
zeigeAufgabe();
}
} catch (_) { ladeAufgabe(); }
}
function restoreTimer(remaining) {
const task = currentTask;
const card = document.getElementById('taskCard');
card.className = 'task-card';
card.innerHTML = `
${badgeHtml(task.nameAktiverMitspieler)}
<div class="task-text" title="${escapeAttr(task.aufgabeText)}">${task.aufgabeText}</div>
<div class="task-footer">
<div class="task-timer-row" id="taskActions">
<div class="timer-big" id="timerValue">${formatTime(remaining)}</div>
<button class="btn-sm-cancel" onclick="timerAbbrechen()">✕ Abbrechen</button>
</div>
${SESSION_BEENDEN_BTN}
</div>`;
let rem = remaining;
timerInterval = setInterval(() => {
rem--;
const el = document.getElementById('timerValue');
if (rem <= 0) {
clearTimer();
if (el) { el.textContent = formatTime(0); el.classList.add('expired'); }
aufgabeAbgeschlossen();
} else {
if (el) el.textContent = formatTime(rem);
}
}, 1000);
}
// ── Gast-Polling: wartet auf aktive Aufgabe für mein Gerät ──
function startGastPoll() {
if (guestPollInterval) return;
const card = document.getElementById('taskCard');
card.className = 'task-card loading';
card.innerHTML = 'Warte auf Aufgabe…';
guestPollInterval = setInterval(pollGastAufgabe, 2500);
}
async function pollGastAufgabe() {
try {
const res = await fetch(`/bdsm/${sessionId}/active-task`);
if (res.status === 204) {
// Keine aktive Aufgabe warten
const card = document.getElementById('taskCard');
if (!card.classList.contains('loading')) {
card.className = 'task-card loading';
card.innerHTML = 'Warte auf Aufgabe…';
}
return;
}
if (!res.ok) return;
const data = await res.json();
const task = JSON.parse(data.taskJson);
if (task.mitspielerId && task.mitspielerId === myMitspielerId) {
// Meine Aufgabe!
clearInterval(guestPollInterval); guestPollInterval = null;
currentTask = task;
if (task.level) {
document.getElementById('levelImg').src = `/img/lvl${task.level}.png`;
document.getElementById('levelDisplay').style.display = '';
}
if (data.elapsedSeconds !== null && data.elapsedSeconds !== undefined && task.timer != null) {
const remaining = task.timer - data.elapsedSeconds;
if (remaining <= 0) { await clearAktiveAufgabe(); gastAufgabeAbgeschlossen(); }
else restoreTimer(Math.floor(remaining));
} else {
zeigeGastAufgabe(task);
}
} else {
// Jemand anderes ist dran
const card = document.getElementById('taskCard');
const name = task.nameAktiverMitspieler || 'Jemand';
card.className = 'task-card loading';
card.innerHTML = `<div style="font-size:1rem;color:var(--color-muted);">${name} ist dran…</div>`;
}
} catch (_) {}
}
function zeigeGastAufgabe(task) {
// Gast sieht seine Aufgabe mit eigenem "Erledigt"-Button
const cb = task.callback;
if (task.timer != null && !data?.elapsedSeconds) zeigeTimerAufgabe(task);
else if (cb && cb.sperreId != null) zeigeSperreAufgabe(task);
else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(task);
else zeigeEinfacheAufgabe(task);
}
async function gastAufgabeAbgeschlossen() {
clearTimer();
await clearAktiveAufgabe();
const card = document.getElementById('taskCard');
card.className = 'task-card loading';
card.innerHTML = 'Aufgabe erledigt warte auf nächste Aufgabe…';
startGastPoll();
}
// ── Host: wenn Aufgabe einem eigenem-Gerät-Spieler gehört ──
let hostPollInterval = null;
function startHostPoll() {
if (hostPollInterval) return;
hostPollInterval = setInterval(pollHostAktiv, 2500);
}
function stopHostPoll() {
if (hostPollInterval) { clearInterval(hostPollInterval); hostPollInterval = null; }
}
async function pollHostAktiv() {
try {
const res = await fetch(`/bdsm/${sessionId}/active-task`);
if (res.status === 204) {
// Gast hat Aufgabe erledigt → Host lädt nächste
stopHostPoll();
ladeAufgabe();
}
} catch (_) {}
}
function zeigeAufgabe() { function zeigeAufgabe() {
const task = currentTask; const task = currentTask;
const cb = task.callback; const cb = task.callback;
saveAktiveAufgabe(task, null);
// Wenn Aufgabe für eigenes-Gerät-Spieler: Host zeigt nur Hinweis
if (!isGuest && task.eigenesGeraet && task.mitspielerId) {
const name = task.nameAktiverMitspieler || 'Jemand';
const card = document.getElementById('taskCard');
card.className = 'task-card';
card.innerHTML = `
<div style="flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:0.5rem;">
<div style="font-size:1.8rem;">📱</div>
<div style="font-size:1rem;color:var(--color-muted);text-align:center;">${name} spielt gerade auf dem eigenen Gerät.</div>
</div>
<div class="task-footer">
<div class="task-btns"></div>
${SESSION_BEENDEN_BTN}
</div>`;
startHostPoll();
return;
}
if (cb && cb.sperreId != null) zeigeSperreAufgabe(task); if (cb && cb.sperreId != null) zeigeSperreAufgabe(task);
else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(task); else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(task);
else if (task.timer != null) zeigeTimerAufgabe(task); else if (task.timer != null) zeigeTimerAufgabe(task);
@@ -380,6 +566,7 @@
actions.innerHTML = ` actions.innerHTML = `
<div class="timer-big" id="timerValue">${formatTime(remaining)}</div> <div class="timer-big" id="timerValue">${formatTime(remaining)}</div>
<button class="btn-sm-cancel" onclick="timerAbbrechen()">✕ Abbrechen</button>`; <button class="btn-sm-cancel" onclick="timerAbbrechen()">✕ Abbrechen</button>`;
saveAktiveAufgabe(task, new Date().toISOString());
timerInterval = setInterval(() => { timerInterval = setInterval(() => {
remaining--; remaining--;
@@ -405,8 +592,10 @@
async function aufgabeAbgeschlossen() { async function aufgabeAbgeschlossen() {
clearTimer(); clearTimer();
await clearAktiveAufgabe();
if (isGuest) { gastAufgabeAbgeschlossen(); return; }
try { try {
const res = await fetch(`/session/sperre/abgelaufene?sessionId=${sessionId}`); const res = await fetch(`/bdsm/sperre/abgelaufene?sessionId=${sessionId}`);
if (res.ok) { if (res.ok) {
const text = await res.text(); const text = await res.text();
const texte = text.split(';').map(t => t.trim()).filter(t => t.length > 0); const texte = text.split(';').map(t => t.trim()).filter(t => t.length > 0);
@@ -429,7 +618,7 @@
const cb = currentTask?.callback; const cb = currentTask?.callback;
if (!cb) return; if (!cb) return;
try { try {
const res = await fetch('/session/sperre', { const res = await fetch('/bdsm/sperre', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...cb, sessionId }), body: JSON.stringify({ ...cb, sessionId }),
@@ -445,7 +634,7 @@
const cb = currentTask?.callback; const cb = currentTask?.callback;
if (!cb) return; if (!cb) return;
try { try {
const res = await fetch('/session/sperre/verlaengern', { const res = await fetch('/bdsm/sperre/verlaengern', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...cb, sessionId }), body: JSON.stringify({ ...cb, sessionId }),
@@ -478,14 +667,14 @@
async function zurueckZuLevel5() { async function zurueckZuLevel5() {
try { try {
await fetch(`/session/${sessionId}/backToLevel5`, { method: 'POST' }); await fetch(`/bdsm/${sessionId}/backToLevel5`, { method: 'POST' });
} catch (_) {} } catch (_) {}
ladeAufgabe(); ladeAufgabe();
} }
async function starteFinale() { async function starteFinale() {
try { try {
const res = await fetch(`/session/sperre/aktive?sessionId=${sessionId}`); const res = await fetch(`/bdsm/sperre/aktive?sessionId=${sessionId}`);
if (res.ok) { if (res.ok) {
const sperren = await res.json(); const sperren = await res.json();
const texte = (sperren || []).map(s => s.releaseText).filter(t => t); const texte = (sperren || []).map(s => s.releaseText).filter(t => t);
@@ -506,7 +695,7 @@
async function ladeFinisher() { async function ladeFinisher() {
try { try {
const res = await fetch(`/session/${sessionId}/finisher`); const res = await fetch(`/bdsm/${sessionId}/finisher`);
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const liste = await res.json(); const liste = await res.json();
naechsterFinisher(liste, 0); naechsterFinisher(liste, 0);
@@ -572,7 +761,7 @@
async function sessionLoeschen() { async function sessionLoeschen() {
versteckeModal(); versteckeModal();
try { try {
await fetch('/session', { await fetch('/bdsm', {
method: 'DELETE', method: 'DELETE',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId }), body: JSON.stringify({ sessionId }),
@@ -583,7 +772,11 @@
} }
// ── Start ── // ── Start ──
ladeAufgabe(); if (isGuest) {
startGastPoll();
} else {
checkAktiveAufgabe();
}
</script> </script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,743 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Mitspieler XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ── Player cards ── */
.player-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1rem;
}
.player-card-header {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 1.25rem;
}
.player-title {
font-weight: 600;
color: var(--color-text);
font-size: 1rem;
}
.player-badge {
background: var(--color-primary);
color: #fff;
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-badge-pending {
background: var(--color-secondary);
color: var(--color-muted);
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-badge-accepted {
background: #1a5c2a;
color: #6fcf97;
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-remove {
margin-left: auto;
background: transparent;
border: 1px solid var(--color-secondary);
color: var(--color-muted);
padding: 0.25rem 0.6rem;
font-size: 0.75rem;
font-weight: normal;
border-radius: 4px;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.player-remove:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: transparent;
}
.btn-invite {
background: transparent;
border: 1px solid var(--color-primary);
color: var(--color-primary);
padding: 0.45rem 1rem;
font-size: 0.875rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn-invite:hover { background: var(--color-primary); color: #fff; }
.btn-cancel-invite {
background: transparent;
border: 1px solid var(--color-secondary);
color: var(--color-muted);
padding: 0.2rem 0.6rem;
font-size: 0.75rem;
font-weight: normal;
border-radius: 4px;
cursor: pointer;
}
.btn-cancel-invite:hover { border-color: var(--color-primary); color: var(--color-primary); background: transparent; }
.pending-info {
text-align: center;
color: var(--color-muted);
font-size: 0.9rem;
padding: 1.5rem 0;
}
.pending-name {
font-weight: 600;
color: var(--color-text);
font-size: 1rem;
margin-bottom: 0.25rem;
}
.pending-mode {
font-size: 0.8rem;
color: var(--color-muted);
}
.card-field { margin-bottom: 1rem; }
.card-field > label {
font-size: 0.8rem;
color: #aaa;
margin: 0 0 0.5rem 0;
display: block;
}
.check-group { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.check-group--two-col { display: grid; grid-template-columns: 1fr 1fr; }
.check-item {
display: inline-flex;
align-items: flex-start;
gap: 0.45rem;
background: var(--color-secondary);
border: 1px solid transparent;
border-radius: 6px;
padding: 0.4rem 0.7rem;
cursor: pointer;
transition: border-color 0.15s;
user-select: none;
}
.check-item.is-checked { border-color: var(--color-primary); }
.check-item input { accent-color: var(--color-primary); width: auto; margin-top: 0.15rem; cursor: pointer; flex-shrink: 0; }
.check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; }
.check-item-desc { display: block; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.1rem; }
.add-player-btn {
width: 100%;
background: transparent;
border: 1px dashed var(--color-secondary);
color: var(--color-muted);
padding: 0.75rem;
border-radius: 10px;
font-size: 0.9rem;
font-weight: normal;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
margin-bottom: 2rem;
}
.add-player-btn:hover { border-color: var(--color-primary); color: var(--color-text); background: transparent; }
.field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; }
/* ── Freunde-Modal ── */
.modal-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.75);
z-index: 1000;
display: flex; align-items: center; justify-content: center;
padding: 1.5rem;
}
.modal-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 1.75rem;
max-width: 420px; width: 100%;
}
.modal-title { font-size: 1rem; font-weight: 700; margin-bottom: 1rem; }
.check-item.is-disabled { opacity: 0.5; pointer-events: none; cursor: default; }
.friend-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; background: var(--color-secondary); flex-shrink: 0; }
.friend-combobox { position: relative; }
.friend-dropdown {
display: none; position: absolute; top: 100%; left: 0; right: 0;
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 8px; max-height: 220px; overflow-y: auto; z-index: 10; margin-top: 0.25rem;
}
.friend-dropdown-item {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.6rem 0.75rem; cursor: pointer; transition: background 0.1s;
font-size: 0.9rem; font-weight: 600;
}
.friend-dropdown-item:hover { background: var(--color-secondary); }
.selected-friend-box {
display: none; margin-top: 0.75rem; padding: 0.6rem 0.75rem;
background: var(--color-secondary); border-radius: 8px;
font-size: 0.9rem; font-weight: 600;
border: 1px solid var(--color-primary); color: var(--color-text);
}
.modal-cancel { margin-top: 0.6rem; width: 100%; }
</style>
</head>
<body class="app">
<div class="modal-overlay" id="friendModal" style="display:none;">
<div class="modal-card">
<div class="modal-title">Freund einladen</div>
<div class="friend-combobox">
<input type="text" id="friendSearch" placeholder="Name eingeben…" autocomplete="off" oninput="filterFreunde(this.value)">
<div class="friend-dropdown" id="friendDropdown"></div>
</div>
<div class="selected-friend-box" id="selectedFriendBox"></div>
<button id="btnEinladen" style="margin-top:1rem; width:100%;" disabled onclick="confirmedEinladen()">Einladen</button>
<button class="secondary modal-cancel" onclick="schliesseFriendModal()">Abbrechen</button>
</div>
</div>
<div class="main">
<div class="content session-setup">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Schritt 2 von 4 Mitspieler</p>
<div class="setup-section">
<h2>Mitspieler</h2>
<div id="playersContainer"></div>
<button class="add-player-btn" onclick="addPlayer()">+ Spieler hinzufügen</button>
</div>
<div class="message" id="message"></div>
<div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsm.html'">← Zurück</button>
<button style="flex:2;" id="weiterBtn" onclick="weiter()">Weiter</button>
</div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
if (!sessionStorage.getItem('bdsm-session-settings')) {
window.location.replace('/bdsm.html');
}
// SetupId erzeugen (persistent über sessionStorage)
if (!sessionStorage.getItem('bdsm-setup-id')) {
sessionStorage.setItem('bdsm-setup-id', crypto.randomUUID());
}
const setupId = sessionStorage.getItem('bdsm-setup-id');
const GESCHLECHTER = [
{ value: 'MAENNLICH', label: 'Männlich' },
{ value: 'WEIBLICH', label: 'Weiblich' },
{ value: 'DIVERS', label: 'Divers' },
];
const ROLLEN = [
{ value: 'AUFGABE_AKTIV', label: 'Aufgabe Aktiv' },
{ value: 'AUFGABE_PASSIV', label: 'Aufgabe Passiv' },
{ value: 'BESTRAFUNG_AKTIV', label: 'Bestrafung Aktiv' },
{ value: 'BESTRAFUNG_PASSIV', label: 'Bestrafung Passiv' },
];
const WERKZEUGE_DEFAULTS = {
MAENNLICH: ['MUND', 'PENIS', 'ANUS', 'UMSCHNALLDILDO'],
WEIBLICH: ['MUND', 'VAGINA', 'ANUS', 'UMSCHNALLDILDO'],
DIVERS: ['MUND', 'ANUS', 'UMSCHNALLDILDO'],
};
const WERKZEUGE = [
{ value: 'MUND', label: 'Mund', desc: 'Gewillt den Mund einzusetzen' },
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina und setzt sie ein' },
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis und setzt ihn ein' },
{ value: 'ANUS', label: 'Anus', desc: 'Gewillt den Anus einzusetzen' },
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
];
const ROLE_LABELS = {
AUFGABE_AKTIV: 'Aufgabe Aktiv', AUFGABE_PASSIV: 'Aufgabe Passiv',
BESTRAFUNG_AKTIV: 'Bestrafung Aktiv', BESTRAFUNG_PASSIV: 'Bestrafung Passiv',
};
let playerSeq = 0;
let playerIds = [];
// { [playerId]: { einladungId, status, inviteeId, inviteeName, mode } | null }
let playerInvitations = {};
let pollIntervalId = null;
let myUserId = null;
let freundeListe = [];
function buildCheckItems(name, items, type, disabled = false) {
return items.map(({ value, label, desc }) => `
<label class="check-item${disabled ? ' is-disabled' : ''}">
<input type="${type}" name="${name}" value="${value}"${disabled ? ' disabled' : ''}>
<span>
<span class="check-item-label">${label}</span>
${desc ? `<span class="check-item-desc">${desc}</span>` : ''}
</span>
</label>`).join('');
}
function createCardHtml(id, prefillName, isSelf) {
const badge = isSelf ? '<span class="player-badge">Du</span>' : '';
const num = playerIds.indexOf(id) + 1;
const nameField = isSelf
? `<input type="text" id="p${id}-name" value="${prefillName}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`
: `<input type="text" id="p${id}-name" value="${prefillName}" placeholder="Name" autocomplete="off">`;
const inviteBtn = isSelf ? '' : `<button class="btn-invite" onclick="oeffneFreundeModal(${id})">👥 Einladen</button>`;
return `
<div class="player-card" id="player-${id}">
<div class="player-card-header">
<span class="player-title">Spieler ${num}</span>
${badge}
${inviteBtn}
<button class="player-remove" onclick="removePlayer(${id})">✕ Entfernen</button>
</div>
<div id="p${id}-body">
${buildPlayerBody(id, nameField, isSelf)}
</div>
</div>`;
}
function buildPlayerBody(id, nameField, genderDisabled = false) {
return `
<div class="card-field">
<label>Name</label>
${nameField}
<div class="field-error" id="p${id}-name-err">Bitte Namen eingeben.</div>
</div>
<div class="card-field">
<label>Geschlecht${genderDisabled ? ' <span style="font-size:0.75rem;color:var(--color-muted);">(unveränderlich)</span>' : ''}</label>
<div class="check-group">${buildCheckItems('p' + id + '-geschlecht', GESCHLECHTER, 'radio', genderDisabled)}</div>
<div class="field-error" id="p${id}-geschlecht-err">Bitte Geschlecht auswählen.</div>
</div>
<div class="card-field">
<label>Spielt mit</label>
<div class="check-group">${buildCheckItems('p' + id + '-spieltmit', GESCHLECHTER, 'checkbox')}</div>
<div class="field-error" id="p${id}-spieltmit-err">Bitte mindestens eine Option wählen.</div>
<div class="field-error" id="p${id}-partner-err">Kein Mitspieler mit passendem Geschlecht vorhanden.</div>
</div>
<div class="card-field">
<label>Rollen</label>
<div class="check-group">${buildCheckItems('p' + id + '-rollen', ROLLEN, 'checkbox')}</div>
<div class="field-error" id="p${id}-rollen-err">Bitte mindestens eine Rolle wählen.</div>
</div>
<div class="card-field">
<label>Verfügbar</label>
<div class="check-group check-group--two-col">${buildCheckItems('p' + id + '-werkzeuge', WERKZEUGE, 'checkbox')}</div>
<div class="field-error" id="p${id}-werkzeuge-err">Bitte mindestens ein Werkzeug wählen.</div>
</div>`;
}
function addPlayer(prefillName = '', isSelf = false) {
playerSeq++;
const id = playerSeq;
playerIds.push(id);
playerInvitations[id] = null;
document.getElementById('playersContainer')
.insertAdjacentHTML('beforeend', createCardHtml(id, prefillName, isSelf));
refreshRemoveButtons();
return id;
}
function removePlayer(id) {
const inv = playerInvitations[id];
if (inv && (inv.status === 'PENDING' || inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST')) {
cancelEinladung(id);
}
document.getElementById('player-' + id)?.remove();
playerIds = playerIds.filter(x => x !== id);
delete playerInvitations[id];
refreshPlayerTitles();
refreshRemoveButtons();
}
function refreshPlayerTitles() {
playerIds.forEach((id, idx) => {
const el = document.querySelector(`#player-${id} .player-title`);
if (el) el.textContent = 'Spieler ' + (idx + 1);
});
}
function refreshRemoveButtons() {
const canRemove = playerIds.length > 2;
playerIds.forEach(id => {
const btn = document.querySelector(`#player-${id} .player-remove`);
if (btn) btn.style.display = canRemove ? '' : 'none';
});
}
document.addEventListener('change', e => {
const input = e.target;
if (input.type !== 'checkbox' && input.type !== 'radio') return;
if (input.type === 'radio') {
document.querySelectorAll(`input[name="${input.name}"]`).forEach(r => {
r.closest('.check-item')?.classList.toggle('is-checked', r.checked);
});
if (input.checked && input.name.endsWith('-geschlecht')) {
const prefix = input.name.slice(0, -'-geschlecht'.length);
const defaults = WERKZEUGE_DEFAULTS[input.value] || [];
document.querySelectorAll(`input[name="${prefix}-werkzeuge"]`).forEach(cb => {
cb.checked = defaults.includes(cb.value);
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
});
}
} else {
input.closest('.check-item')?.classList.toggle('is-checked', input.checked);
}
});
function getChecked(name) {
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(el => el.value);
}
function setFieldError(id, show) {
const el = document.getElementById(id);
if (el) el.style.display = show ? 'block' : 'none';
}
// ── Freunde-Modal ──
let currentInvitePlayerId = null;
let selectedFriend = null; // { userId, name }
async function oeffneFreundeModal(playerId) {
currentInvitePlayerId = playerId;
selectedFriend = null;
document.getElementById('friendSearch').value = '';
document.getElementById('friendDropdown').style.display = 'none';
document.getElementById('friendDropdown').innerHTML = '';
document.getElementById('selectedFriendBox').style.display = 'none';
document.getElementById('selectedFriendBox').textContent = '';
document.getElementById('btnEinladen').disabled = true;
document.getElementById('friendModal').style.display = 'flex';
if (freundeListe.length === 0) {
try {
const res = await fetch('/social/friends');
freundeListe = res.ok ? await res.json() : [];
} catch (_) { freundeListe = []; }
}
}
function filterFreunde(query) {
selectedFriend = null;
document.getElementById('selectedFriendBox').style.display = 'none';
document.getElementById('btnEinladen').disabled = true;
const dropdown = document.getElementById('friendDropdown');
dropdown.innerHTML = '';
const q = query.trim().toLowerCase();
if (!q) { dropdown.style.display = 'none'; return; }
const matches = freundeListe.filter(f => (f.user.name || '').toLowerCase().includes(q));
if (!matches.length) {
dropdown.innerHTML = '<div style="padding:0.6rem 0.75rem;color:var(--color-muted);font-size:0.9rem;">Keine Treffer.</div>';
dropdown.style.display = 'block';
return;
}
matches.forEach(f => {
const item = document.createElement('div');
item.className = 'friend-dropdown-item';
item.addEventListener('click', () => selectFriend(f.user.userId, f.user.name || 'Unbekannt'));
if (f.user.profilePicture) {
const img = document.createElement('img');
img.className = 'friend-avatar';
img.src = 'data:image/png;base64,' + f.user.profilePicture;
img.alt = '';
item.appendChild(img);
} else {
const av = document.createElement('div');
av.className = 'friend-avatar';
item.appendChild(av);
}
const span = document.createElement('span');
span.textContent = f.user.name || 'Unbekannt';
item.appendChild(span);
dropdown.appendChild(item);
});
dropdown.style.display = 'block';
}
function selectFriend(userId, name) {
selectedFriend = { userId, name };
document.getElementById('friendSearch').value = name;
document.getElementById('friendDropdown').style.display = 'none';
const box = document.getElementById('selectedFriendBox');
box.textContent = '✓ ' + name;
box.style.display = 'block';
document.getElementById('btnEinladen').disabled = false;
}
async function confirmedEinladen() {
if (!selectedFriend) return;
await einladen(selectedFriend.userId, selectedFriend.name);
}
function schliesseFriendModal() {
document.getElementById('friendModal').style.display = 'none';
currentInvitePlayerId = null;
selectedFriend = null;
}
async function einladen(inviteeId, inviteeName) {
const id = currentInvitePlayerId;
schliesseFriendModal();
if (!id) return;
try {
const res = await fetch('/bdsm/einladung', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ setupId, slotIndex: id, inviteeId }),
});
if (!res.ok) throw new Error();
const data = await res.json();
playerInvitations[id] = { einladungId: data.einladungId, status: 'PENDING', inviteeId, inviteeName };
renderPending(id);
startPoll();
} catch (_) {
showMessage('Einladung konnte nicht gesendet werden.', 'error');
}
}
function renderPending(id) {
const inv = playerInvitations[id];
if (!inv) return;
const body = document.getElementById(`p${id}-body`);
if (!body) return;
const headerInvBtn = document.querySelector(`#player-${id} .btn-invite`);
if (headerInvBtn) headerInvBtn.style.display = 'none';
if (inv.status === 'PENDING') {
// Badge
const header = document.querySelector(`#player-${id} .player-card-header`);
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
header.insertAdjacentHTML('afterbegin', `<span class="player-badge-pending">Ausstehend</span>`);
body.innerHTML = `
<div class="pending-info">
<div class="pending-name">${inv.inviteeName}</div>
<div>Einladung wurde gesendet warte auf Antwort…</div>
<button class="btn-cancel-invite" style="margin-top:1rem;" onclick="cancelEinladung(${id})">Einladung abbrechen</button>
</div>`;
} else if (inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST') {
const header = document.querySelector(`#player-${id} .player-card-header`);
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
const modeLabel = inv.status === 'ACCEPTED_OWN' ? 'Eigenes Gerät' : 'Host-Gerät';
header.insertAdjacentHTML('afterbegin', `<span class="player-badge-accepted">✓ ${modeLabel}</span>`);
// Name readonly, Geschlecht gesperrt (kommt vom Profil der eingeladenen Person)
const nameField = `<input type="text" id="p${id}-name" value="${inv.inviteeName}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`;
body.innerHTML = buildPlayerBody(id, nameField, true);
// Defaults laden falls verfügbar
if (inv.defaults) {
restorePlayer(id, {
geschlecht: inv.defaults.geschlecht,
spieltMit: inv.defaults.spieltMit || [],
rollen: inv.defaults.rollen || [],
werkzeuge: inv.defaults.werkzeuge || [],
});
}
} else if (inv.status === 'DECLINED' || inv.status === 'CANCELLED') {
// Slot wieder freigeben
const header = document.querySelector(`#player-${id} .player-card-header`);
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
if (headerInvBtn) headerInvBtn.style.display = '';
playerInvitations[id] = null;
body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
}
}
async function cancelEinladung(id) {
const inv = playerInvitations[id];
if (!inv) return;
await fetch(`/bdsm/einladung/${inv.einladungId}`, { method: 'DELETE' }).catch(() => {});
playerInvitations[id] = null;
// UI zurücksetzen
const header = document.querySelector(`#player-${id} .player-card-header`);
if (header) {
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
const invBtn = header.querySelector('.btn-invite');
if (invBtn) invBtn.style.display = '';
}
const body = document.getElementById(`p${id}-body`);
if (body) body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
}
// ── Polling ──
function startPoll() {
if (pollIntervalId) return;
pollIntervalId = setInterval(pollEinladungen, 3000);
}
function stopPoll() {
if (pollIntervalId) { clearInterval(pollIntervalId); pollIntervalId = null; }
}
async function pollEinladungen() {
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
if (!hasPending) { stopPoll(); return; }
try {
const res = await fetch(`/bdsm/einladung?setupId=${setupId}`);
if (!res.ok) return;
const liste = await res.json();
for (const e of liste) {
const id = playerIds.find(pid => {
const inv = playerInvitations[pid];
return inv && inv.einladungId === e.einladungId;
});
if (!id) continue;
const inv = playerInvitations[id];
if (!inv || inv.status === e.status) continue;
inv.status = e.status;
if (e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST') {
// Defaults der eingeladenen Person laden
try {
const dRes = await fetch(`/user/${inv.inviteeId}/bdsm-defaults`);
if (dRes.ok) inv.defaults = await dRes.json();
} catch (_) {}
}
renderPending(id);
}
} catch (_) {}
updateWeiterBtn();
}
function updateWeiterBtn() {
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
document.getElementById('weiterBtn').disabled = hasPending;
}
// ── Validation & Weiter ──
function weiter() {
hideMessage();
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
if (hasPending) {
showMessage('Bitte warte, bis alle Einladungen beantwortet wurden.', 'error');
return;
}
let valid = true;
playerIds.forEach(id => setFieldError(`p${id}-partner-err`, false));
const mitspieler = playerIds.map(id => {
const inv = playerInvitations[id];
const name = document.getElementById(`p${id}-name`)?.value.trim() || '';
const geschlecht = getChecked(`p${id}-geschlecht`);
const spieltMit = getChecked(`p${id}-spieltmit`);
const rollen = getChecked(`p${id}-rollen`);
const werkzeuge = getChecked(`p${id}-werkzeuge`);
setFieldError(`p${id}-name-err`, !name);
setFieldError(`p${id}-geschlecht-err`, geschlecht.length === 0);
setFieldError(`p${id}-spieltmit-err`, spieltMit.length === 0);
setFieldError(`p${id}-rollen-err`, rollen.length === 0);
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
if (!name || geschlecht.length === 0 || spieltMit.length === 0 || rollen.length === 0 || werkzeuge.length === 0) {
valid = false;
}
return {
name,
geschlecht: geschlecht[0] || null,
spieltMit, rollen, werkzeuge,
userId: inv ? inv.inviteeId : null,
eigenesGeraet: inv ? inv.status === 'ACCEPTED_OWN' : false,
};
});
if (!valid) { showMessage('Bitte alle Felder für jeden Spieler ausfüllen.', 'error'); return; }
const allRoles = new Set(mitspieler.flatMap(p => p.rollen));
const missingRoles = Object.keys(ROLE_LABELS).filter(r => !allRoles.has(r));
if (missingRoles.length > 0) {
showMessage('Folgende Rollen müssen mindestens einmal vergeben sein: ' +
missingRoles.map(r => ROLE_LABELS[r]).join(', '), 'error');
return;
}
let partnerFehler = false;
mitspieler.forEach((player, i) => {
const andereGeschlechter = mitspieler.filter((_, j) => j !== i).map(p => p.geschlecht);
const hatPartner = player.spieltMit.some(g => andereGeschlechter.includes(g));
if (!hatPartner) { setFieldError(`p${playerIds[i]}-partner-err`, true); partnerFehler = true; }
});
if (partnerFehler) { showMessage('Mindestens ein Spieler hat keinen kompatiblen Mitspieler.', 'error'); return; }
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
sessionStorage.setItem('bdsm-session-setup', JSON.stringify({ settings, mitspieler }));
window.location.href = '/bdsmtasks.html';
}
function showMessage(text, type) {
const el = document.getElementById('message');
el.textContent = text; el.className = `message ${type}`; el.style.display = 'block';
}
function hideMessage() { document.getElementById('message').style.display = 'none'; }
function restorePlayer(id, data) {
if (data.geschlecht) {
const radio = document.querySelector(`input[name="p${id}-geschlecht"][value="${data.geschlecht}"]`);
if (radio) { radio.checked = true; radio.closest('.check-item')?.classList.add('is-checked'); }
}
(data.spieltMit || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-spieltmit"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
(data.rollen || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-rollen"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
(data.werkzeuge || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-werkzeuge"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
}
// ── Init ──
const savedSetup = sessionStorage.getItem('bdsm-session-setup');
if (savedSetup) {
const { mitspieler } = JSON.parse(savedSetup);
mitspieler.forEach((p, i) => {
const id = addPlayer(p.name, i === 0);
restorePlayer(id, p);
});
} else {
Promise.all([
fetch('/login/me').then(r => r.ok ? r.json() : null).catch(() => null),
fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}))
]).then(([user, defaults]) => {
myUserId = user?.userId || null;
addPlayer(user ? user.name : '', true);
addPlayer();
const selfId = playerIds[0];
restorePlayer(selfId, {
geschlecht: user?.geschlecht || null,
spieltMit: defaults.spieltMit || [],
rollen: defaults.rollen || [],
werkzeuge: defaults.werkzeuge || [],
});
});
}
</script>
</body>
</html>

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Aufgaben-Gruppen XXX The Game</title> <title>BDSM Game Aufgaben-Gruppen XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
.session-setup { max-width: 700px; } .session-setup { }
.setup-section { margin-bottom: 2.5rem; } .setup-section { margin-bottom: 2.5rem; }
.setup-section h2 { .setup-section h2 {
@@ -119,7 +119,7 @@
<div style="position:relative; margin-top:2rem;"> <div style="position:relative; margin-top:2rem;">
<div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div> <div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div>
<div style="display:flex; gap:1rem;"> <div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/sessionbdsmplayers.html'">← Zurück</button> <button style="flex:1;" class="secondary" onclick="window.location.href='/bdsmplayers.html'">← Zurück</button>
<button style="flex:2;" onclick="weiter()">Weiter</button> <button style="flex:2;" onclick="weiter()">Weiter</button>
</div> </div>
</div> </div>
@@ -130,7 +130,7 @@
<script src="/js/sidebar.js"></script> <script src="/js/sidebar.js"></script>
<script> <script>
if (!sessionStorage.getItem('bdsm-session-setup')) { if (!sessionStorage.getItem('bdsm-session-setup')) {
window.location.replace('/sessionbdsm.html'); window.location.replace('/bdsm.html');
} }
const savedGruppen = new Set(JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || '[]')); const savedGruppen = new Set(JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || '[]'));
@@ -308,18 +308,19 @@
} }
sessionStorage.setItem('bdsm-session-gruppen', JSON.stringify(selected)); sessionStorage.setItem('bdsm-session-gruppen', JSON.stringify(selected));
window.location.href = '/sessionbdsmtoys.html'; window.location.href = '/bdsmtoys.html';
} }
Promise.all([ Promise.all([
fetch('/gruppe/list/user?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }), fetch('/gruppe/list/user?page=0&size=500').then(r => r.ok ? r.json() : (console.warn('[bdsmtasks] user HTTP', r.status), { content: [] })),
fetch('/abo/list?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }), fetch('/abo/list?page=0&size=500').then(r => r.ok ? r.json() : (console.warn('[bdsmtasks] abo HTTP', r.status), { content: [] })),
fetch('/gruppe/list/system?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }), fetch('/gruppe/list/system?page=0&size=500').then(r => r.ok ? r.json() : (console.warn('[bdsmtasks] system HTTP', r.status), { content: [] })),
]).then(([own, abo, system]) => { ]).then(([own, abo, system]) => {
console.log('[bdsmtasks] own:', own, 'abo:', abo, 'system:', system);
renderList('listOwn', own.content || []); renderList('listOwn', own.content || []);
renderList('listSubscribed', abo.content || []); renderList('listSubscribed', abo.content || []);
renderList('listSystem', system.content || []); renderList('listSystem', system.content || []);
}); }).catch(err => console.error('[bdsmtasks] Fehler beim Laden:', err));
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Toys XXX The Game</title> <title>BDSM Game Toys XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
.session-setup { max-width: 700px; } .session-setup { }
.setup-section { margin-bottom: 2.5rem; } .setup-section { margin-bottom: 2.5rem; }
.setup-section h2 { .setup-section h2 {
@@ -74,7 +74,7 @@
<div style="position:relative; margin-top:2rem;"> <div style="position:relative; margin-top:2rem;">
<div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div> <div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div>
<div style="display:flex; gap:1rem;"> <div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/sessionbdsmtasks.html'">← Zurück</button> <button style="flex:1;" class="secondary" onclick="window.location.href='/bdsmtasks.html'">← Zurück</button>
<button style="flex:2;" onclick="spielStarten()">Spiel starten</button> <button style="flex:2;" onclick="spielStarten()">Spiel starten</button>
</div> </div>
</div> </div>
@@ -85,7 +85,7 @@
<script src="/js/sidebar.js"></script> <script src="/js/sidebar.js"></script>
<script> <script>
const savedGruppen = JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || 'null'); const savedGruppen = JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || 'null');
if (!savedGruppen) window.location.replace('/sessionbdsm.html'); if (!savedGruppen) window.location.replace('/bdsm.html');
// Previously saved toy selection (when navigating back from game page) // Previously saved toy selection (when navigating back from game page)
const savedToysRaw = sessionStorage.getItem('bdsm-session-toys'); const savedToysRaw = sessionStorage.getItem('bdsm-session-toys');
@@ -241,7 +241,7 @@
try { try {
// 1. Session anlegen // 1. Session anlegen
const sessionRes = await fetch('/session', { const sessionRes = await fetch('/bdsm', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -249,6 +249,7 @@
wahrscheinlichkeitSperre: settings.wahrscheinlichkeitSperre, wahrscheinlichkeitSperre: settings.wahrscheinlichkeitSperre,
aufgabenProLevel: settings.aufgabenProLevel, aufgabenProLevel: settings.aufgabenProLevel,
zeitfaktorZeitstrafen: settings.zeitfaktorZeitstrafen, zeitfaktorZeitstrafen: settings.zeitfaktorZeitstrafen,
setupId: sessionStorage.getItem('bdsm-setup-id'),
}), }),
}); });
if (!sessionRes.ok) throw new Error('Session konnte nicht angelegt werden.'); if (!sessionRes.ok) throw new Error('Session konnte nicht angelegt werden.');
@@ -258,7 +259,7 @@
// 2. Mitspieler hinzufügen // 2. Mitspieler hinzufügen
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup')); const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
for (const p of setup.mitspieler) { for (const p of setup.mitspieler) {
const res = await fetch(`/session/${sessionId}/mitspieler`, { const res = await fetch(`/bdsm/${sessionId}/mitspieler`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -267,13 +268,15 @@
spieltMit: p.spieltMit, spieltMit: p.spieltMit,
rollen: p.rollen, rollen: p.rollen,
verfuegbareWerkzeuge: p.werkzeuge, verfuegbareWerkzeuge: p.werkzeuge,
userId: p.userId || null,
eigenesGeraet: p.eigenesGeraet || false,
}), }),
}); });
if (!res.ok) throw new Error(`Mitspieler "${p.name}" konnte nicht hinzugefügt werden.`); if (!res.ok) throw new Error(`Mitspieler "${p.name}" konnte nicht hinzugefügt werden.`);
} }
// 3. Aufgaben setzen // 3. Aufgaben setzen
const aufgabenRes = await fetch(`/session/${sessionId}/aufgaben`, { const aufgabenRes = await fetch(`/bdsm/${sessionId}/aufgaben`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(gameContent), body: JSON.stringify(gameContent),
@@ -281,7 +284,7 @@
if (!aufgabenRes.ok) throw new Error('Aufgaben konnten nicht gespeichert werden.'); if (!aufgabenRes.ok) throw new Error('Aufgaben konnten nicht gespeichert werden.');
sessionStorage.setItem('bdsm-session-id', sessionId); sessionStorage.setItem('bdsm-session-id', sessionId);
window.location.href = '/sessionbdsmingame.html'; window.location.href = '/bdsmingame.html';
} catch (e) { } catch (e) {
showMessage(e.message, 'error'); showMessage(e.message, 'error');
btn.disabled = false; btn.disabled = false;

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Warten XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.wait-card {
text-align: center;
padding: 3rem 1rem;
}
.wait-icon { font-size: 3rem; margin-bottom: 1.5rem; animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
.wait-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 0.75rem; }
.wait-sub { font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; margin-bottom: 2rem; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content wait-card">
<div class="wait-icon"></div>
<div class="wait-title">Warte auf Spielstart…</div>
<div class="wait-sub" id="sub">Der Host startet das Spiel in Kürze. Diese Seite aktualisiert sich automatisch.</div>
<div class="message" id="message" style="display:none;"></div>
<button class="secondary" onclick="abbrechen()">Abbrechen</button>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
const params = new URLSearchParams(location.search);
const einladungId = params.get('id');
if (!einladungId) window.location.replace('/userhome.html');
let pollInterval = null;
async function pruefen() {
try {
const res = await fetch(`/bdsm/einladung/${einladungId}`);
if (!res.ok) return;
const data = await res.json();
if (data.status === 'CANCELLED') {
stopPoll();
zeigeFehler('Die Einladung wurde abgebrochen.');
return;
}
if (data.sessionId) {
stopPoll();
// Meinen Mitspieler laden
try {
const mRes = await fetch(`/bdsm/${data.sessionId}/mitspieler/me`);
if (mRes.ok) {
const mData = await mRes.json();
sessionStorage.setItem('bdsm-guest-mitspieler-id', mData.mitspielerId);
sessionStorage.setItem('bdsm-guest-name', mData.name);
}
} catch (_) {}
sessionStorage.setItem('bdsm-session-id', data.sessionId);
sessionStorage.setItem('bdsm-is-guest', 'true');
window.location.replace('/bdsmingame.html');
}
} catch (_) {}
}
function stopPoll() {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
}
function zeigeFehler(text) {
document.getElementById('sub').style.display = 'none';
const el = document.getElementById('message');
el.textContent = text;
el.className = 'message error';
el.style.display = '';
}
async function abbrechen() {
stopPoll();
await fetch(`/bdsm/einladung/${einladungId}`, { method: 'DELETE' }).catch(() => {});
window.location.href = '/userhome.html';
}
// Sofort prüfen, dann alle 3 Sekunden
pruefen();
pollInterval = setInterval(pruefen, 3000);
</script>
</body>
</html>

View File

@@ -406,11 +406,11 @@
</div> </div>
</div> </div>
<!-- Tabs: Feed | Pinnwand | Lock-Historie --> <!-- Tabs: Feed | Pinnwand | Spielhistorie -->
<div class="profil-tabs" style="margin-top:1.25rem;"> <div class="profil-tabs" style="margin-top:1.25rem;">
<button class="profil-tab-btn active" id="tabBtnPosts" onclick="switchProfilTab('posts', this)">Feed</button> <button class="profil-tab-btn active" id="tabBtnPosts" onclick="switchProfilTab('posts', this)">Feed</button>
<button class="profil-tab-btn" id="tabBtnPinnwand" onclick="switchProfilTab('pinnwand', this)">Pinnwand</button> <button class="profil-tab-btn" id="tabBtnPinnwand" onclick="switchProfilTab('pinnwand', this)">Pinnwand</button>
<button class="profil-tab-btn" id="tabBtnLockHistory" onclick="switchProfilTab('lockhistory', this); loadLockHistory()">Lock-Historie</button> <button class="profil-tab-btn" id="tabBtnGameHistory" onclick="switchProfilTab('gamehistory', this); loadGameHistory()">Spielhistorie</button>
</div> </div>
<!-- Feed Tab (vorausgewählt) --> <!-- Feed Tab (vorausgewählt) -->
@@ -429,10 +429,10 @@
<div id="pinnwandList"></div> <div id="pinnwandList"></div>
</div> </div>
<!-- Lock-Historie Tab --> <!-- Spielhistorie Tab -->
<div class="profil-tab-panel" id="tab-lockhistory"> <div class="profil-tab-panel" id="tab-gamehistory">
<div id="lockHistoryList" style="margin-top:0.75rem;"></div> <div id="gameHistoryList" style="margin-top:0.75rem;"></div>
<p id="lockHistoryEmpty" style="color:var(--color-muted);font-size:0.9rem;display:none;">Keine abgeschlossenen Locks vorhanden.</p> <p id="gameHistoryEmpty" style="color:var(--color-muted);font-size:0.9rem;display:none;">Keine abgeschlossenen Locks vorhanden.</p>
</div> </div>
</div> </div>
@@ -467,6 +467,7 @@
// ── State ── // ── State ──
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
let targetUserId = params.get('userId'); let targetUserId = params.get('userId');
const previewMode = params.get('preview'); // 'FREUND' | 'UNBEKANNT' | null
let myUserId = null; let myUserId = null;
let isOwnProfile = false; let isOwnProfile = false;
let profileData = null; let profileData = null;
@@ -524,25 +525,52 @@
} }
myUserId = me ? me.userId : null; myUserId = me ? me.userId : null;
isOwnProfile = me && me.userId === profile.userId; isOwnProfile = !previewMode && me && me.userId === profile.userId;
profileData = profile; profileData = profile;
allImages = images; allImages = images;
// ── Preview-Modus: friendStatus simulieren ──
if (previewMode) {
profile.friendStatus = (previewMode === 'FREUND') ? 'FRIEND' : 'NONE';
showPreviewBanner(previewMode);
}
const isFriend = profile.friendStatus === 'FRIEND';
document.title = profile.name + ' XXX The Game'; document.title = profile.name + ' XXX The Game';
renderHeader(profile); renderHeader(profile);
renderGallery();
loadFriends(); // ── Galerie ──
if (profile.beschreibung) { if (canSee(profile.sichtbarkeitGalerie, isFriend, isOwnProfile)) {
renderGallery();
}
// ── Freunde ──
if (canSee(profile.sichtbarkeitFreunde, isFriend, isOwnProfile)) {
loadFriends();
}
if (profile.beschreibung && canSee(profile.sichtbarkeitGrunddaten, isFriend, isOwnProfile)) {
document.getElementById('beschreibungLabel').style.display = ''; document.getElementById('beschreibungLabel').style.display = '';
const el = document.getElementById('profilBeschreibung'); const el = document.getElementById('profilBeschreibung');
el.style.display = ''; el.style.display = '';
el.textContent = profile.beschreibung; el.textContent = profile.beschreibung;
} }
await loadPinnwand();
// ── Tabs: Feed, Pinnwand, Spielhistorie ──
applyTabPrivacy(profile, isFriend);
if (canSee(profile.sichtbarkeitPinnwand, isFriend, isOwnProfile)) {
await loadPinnwand();
}
document.getElementById('profileView').style.display = ''; document.getElementById('profileView').style.display = '';
// Feed-Tab ist vorausgewählt → sofort laden
loadProfilPosts(); // Feed-Tab ist vorausgewählt → sofort laden (nur wenn sichtbar)
profilPostsObserver.observe(document.getElementById('profilPostsSentinel')); if (canSee(profile.sichtbarkeitFeed, isFriend, isOwnProfile)) {
loadProfilPosts();
profilPostsObserver.observe(document.getElementById('profilPostsSentinel'));
}
} catch { } catch {
document.getElementById('loadingHint').textContent = 'Fehler beim Laden.'; document.getElementById('loadingHint').textContent = 'Fehler beim Laden.';
document.getElementById('loadingHint').style.display = ''; document.getElementById('loadingHint').style.display = '';
@@ -568,14 +596,16 @@
d.innerHTML = `<span class="label">${label}</span><span class="value">${esc(value)}</span>`; d.innerHTML = `<span class="label">${label}</span><span class="value">${esc(value)}</span>`;
tags.appendChild(d); tags.appendChild(d);
}; };
if (profile.alter) addTag('Alter', profile.alter + ' J.'); const grunddatenVisible = canSee(profile.sichtbarkeitGrunddaten, profile.friendStatus === 'FRIEND', isOwnProfile);
if (profile.groesse) addTag('Größe', profile.groesse + ' cm'); if (grunddatenVisible && profile.alter) addTag('Alter', profile.alter + ' J.');
if (profile.gewicht) addTag('Gewicht', profile.gewicht + ' kg'); if (grunddatenVisible && profile.groesse) addTag('Größe', profile.groesse + ' cm');
if (profile.geschlecht) addTag('Geschlecht', GESCHLECHT_LABEL[profile.geschlecht] || profile.geschlecht); if (grunddatenVisible && profile.gewicht) addTag('Gewicht', profile.gewicht + ' kg');
if (profile.neigung) addTag('Neigung', NEIGUNG_LABEL[profile.neigung] || profile.neigung); if (grunddatenVisible && profile.geschlecht) addTag('Geschlecht', GESCHLECHT_LABEL[profile.geschlecht] || profile.geschlecht);
if (profile.beziehungsstatus) addTag('Beziehung', BEZIEHUNG_LABEL[profile.beziehungsstatus] || profile.beziehungsstatus); if (grunddatenVisible && profile.neigung) addTag('Neigung', NEIGUNG_LABEL[profile.neigung] || profile.neigung);
if (profile.lockeeXp > 0) addTag('🔒 Lockee XP', profile.lockeeXp + ' XP'); if (grunddatenVisible && profile.beziehungsstatus) addTag('Beziehung', BEZIEHUNG_LABEL[profile.beziehungsstatus] || profile.beziehungsstatus);
if (profile.keyholderXp > 0) addTag('🔑 Keyholder XP', profile.keyholderXp + ' XP'); const xpVisible = canSee(profile.sichtbarkeitXp, profile.friendStatus === 'FRIEND', isOwnProfile);
if (xpVisible && profile.lockeeXp > 0) addTag('🔒 Lockee XP', profile.lockeeXp + ' XP');
if (xpVisible && profile.keyholderXp > 0) addTag('🔑 Keyholder XP', profile.keyholderXp + ' XP');
// Action buttons // Action buttons
const actions = document.getElementById('profileActions'); const actions = document.getElementById('profileActions');
@@ -596,6 +626,50 @@
} }
} }
// ── Privacy helpers ──
function canSee(sichtbarkeit, isFriend, isOwn) {
if (isOwn || !sichtbarkeit) return true;
if (sichtbarkeit === 'ALLE') return true;
if (sichtbarkeit === 'NUR_FREUNDE') return isFriend;
// NUR_ICH
return false;
}
function applyTabPrivacy(profile, isFriend) {
const showFeed = canSee(profile.sichtbarkeitFeed, isFriend, isOwnProfile);
const showPinnwand = canSee(profile.sichtbarkeitPinnwand, isFriend, isOwnProfile);
const showHistory = canSee(profile.sichtbarkeitLockhistorie, isFriend, isOwnProfile);
const btnFeed = document.getElementById('tabBtnPosts');
const btnPinnwand = document.getElementById('tabBtnPinnwand');
const btnHistory = document.getElementById('tabBtnGameHistory');
if (!showFeed) { btnFeed.style.display = 'none'; document.getElementById('tab-posts').classList.remove('active'); }
if (!showPinnwand) { btnPinnwand.style.display = 'none'; }
if (!showHistory) { btnHistory.style.display = 'none'; }
// Ersten sichtbaren Tab aktivieren
if (!showFeed) {
btnFeed.classList.remove('active');
document.getElementById('tab-posts').classList.remove('active');
if (showPinnwand) {
btnPinnwand.classList.add('active');
document.getElementById('tab-pinnwand').classList.add('active');
} else if (showHistory) {
btnHistory.classList.add('active');
document.getElementById('tab-gamehistory').classList.add('active');
}
}
}
function showPreviewBanner(mode) {
const banner = document.createElement('div');
banner.style.cssText = 'background:var(--color-secondary);border:1px solid var(--color-primary);border-radius:8px;padding:0.65rem 1rem;margin-bottom:1rem;font-size:0.88rem;display:flex;align-items:center;justify-content:space-between;gap:0.75rem;';
const label = mode === 'FREUND' ? '👥 Vorschau aus Freundessicht' : '👤 Vorschau aus Sicht einer fremden Person';
banner.innerHTML = `<span>${label}</span><a href="/einstellungen.html" style="font-size:0.82rem;color:var(--color-primary);">← Einstellungen</a>`;
document.getElementById('profileView').prepend(banner);
}
// ── Tab switching ── // ── Tab switching ──
function switchProfilTab(name, btn) { function switchProfilTab(name, btn) {
document.querySelectorAll('.profil-tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.profil-tab-btn').forEach(b => b.classList.remove('active'));
@@ -777,47 +851,60 @@
</div>`; </div>`;
} }
// ── Lock-Historie ── // ── Spielhistorie ──
let lockHistoryLoaded = false; const GAME_TYPE_ICON = {
async function loadLockHistory() { CARDLOCK: '<span style="position:relative;display:inline-block;line-height:1;"><img src="/img/card.png" style="width:2.7rem;height:2.7rem;object-fit:contain;display:block;"><span style="position:absolute;bottom:-2px;right:-4px;font-size:1.3rem;line-height:1;">🔒</span></span>',
if (lockHistoryLoaded) return; TIMELOCK: '<span style="position:relative;display:inline-block;line-height:1;"><span style="font-size:2.7rem;">⏰</span><span style="position:absolute;bottom:-2px;right:-4px;font-size:1.3rem;line-height:1;">🔒</span></span>',
lockHistoryLoaded = true; BDSM: '⛓️',
const list = document.getElementById('lockHistoryList'); VANILLA: '❤️'
const empty = document.getElementById('lockHistoryEmpty'); };
const ROLE_BADGE = { KEYHOLDER: '🔑', LOCKEE: '🔒', PLAYER: '' };
let gameHistoryLoaded = false;
async function loadGameHistory() {
if (gameHistoryLoaded) return;
gameHistoryLoaded = true;
const list = document.getElementById('gameHistoryList');
const empty = document.getElementById('gameHistoryEmpty');
list.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Lädt…</p>'; list.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Lädt…</p>';
try { try {
const res = await fetch('/lockhistory?userId=' + targetUserId); const res = await fetch('/gamehistory?userId=' + targetUserId);
if (!res.ok) { list.innerHTML = ''; return; } if (!res.ok) { list.innerHTML = ''; return; }
const entries = await res.json(); const entries = await res.json();
list.innerHTML = ''; list.innerHTML = '';
if (entries.length === 0) { empty.style.display = ''; return; } if (entries.length === 0) { empty.style.display = ''; return; }
list.innerHTML = entries.map(e => { list.innerHTML = entries.map(e => {
const icon = e.role === 'KEYHOLDER' ? '🔑' : '🔒'; const gameIconRaw = GAME_TYPE_ICON[e.gameType] || '🎮';
const partner = e.role === 'KEYHOLDER' const gameIcon = (e.gameType === 'CARDLOCK' || e.gameType === 'TIMELOCK')
? (e.lockeeName ? `<span style="color:var(--color-muted);font-size:0.78rem;">Lockee: ${esc(e.lockeeName)}</span>` : '') ? gameIconRaw
: (e.keyholderName ? `<span style="color:var(--color-muted);font-size:0.78rem;">Keyholder: ${esc(e.keyholderName)}</span>` : '<span style="color:var(--color-muted);font-size:0.78rem;">Self-Lock</span>'); : `<span style="font-size:2.7rem;line-height:1;">${gameIconRaw}</span>`;
const days = Math.floor(e.durationMinutes / 1440);
const hours = Math.floor((e.durationMinutes % 1440) / 60); const days = Math.floor(e.durationMinutes / 1440);
const mins = e.durationMinutes % 60; const hours = Math.floor((e.durationMinutes % 1440) / 60);
const dur = days > 0 const mins = e.durationMinutes % 60;
const dur = days > 0
? `${days}d ${hours}h ${mins}min` ? `${days}d ${hours}h ${mins}min`
: hours > 0 ? `${hours}h ${mins}min` : `${mins}min`; : hours > 0 ? `${hours}h ${mins}min` : `${mins}min`;
const avatar = e.partnerPic
? `<img src="data:image/jpeg;base64,${e.partnerPic}" style="width:40px;height:40px;border-radius:50%;object-fit:cover;display:block;">` const participants = (e.participants || []).map(p => {
: `<div style="width:40px;height:40px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:1.1rem;">👤</div>`; const badge = ROLE_BADGE[p.role] || '';
return `<div style="display:flex;align-items:center;gap:0.75rem;padding:0.65rem 0;border-bottom:1px solid var(--color-secondary);"> const img = p.picture
<div style="position:relative;width:40px;height:40px;flex-shrink:0;"> ? `<img src="data:image/png;base64,${p.picture}" style="width:40px;height:40px;border-radius:50%;object-fit:cover;display:block;">`
${avatar} : `<div style="width:40px;height:40px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0;">👤</div>`;
<span style="position:absolute;top:-3px;left:-3px;font-size:1.4rem;line-height:1;">${icon}</span> return `<a href="/benutzer.html?userId=${esc(p.userId)}" style="position:relative;flex-shrink:0;text-decoration:none;" title="${esc(p.name || '')} (${p.role})">
</div> ${img}
${badge ? `<span style="position:absolute;top:-4px;right:-4px;font-size:0.9rem;line-height:1;">${badge}</span>` : ''}
</a>`;
}).join('');
return `<div style="display:flex;align-items:flex-start;gap:0.85rem;padding:0.75rem 0;border-bottom:1px solid var(--color-secondary);">
<div style="flex-shrink:0;width:3rem;text-align:center;">${gameIcon}</div>
<div style="flex:1;min-width:0;"> <div style="flex:1;min-width:0;">
<div style="font-weight:600;font-size:0.92rem;">${esc(e.lockName) || 'Unbenanntes Lock'}</div> <div style="font-weight:600;font-size:0.92rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${esc(e.gameName) || 'Unbenannt'}</div>
<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-top:0.15rem;"> <div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.15rem;">⏱ ${dur} &nbsp;·&nbsp; ${new Date(e.endTime).toLocaleDateString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric'})}</div>
${partner}
<span style="color:var(--color-muted);font-size:0.78rem;">⏱ ${dur}</span>
<span style="color:var(--color-muted);font-size:0.78rem;">${fmtDate(e.unlockTime)}</span>
</div>
</div> </div>
<div style="display:flex;gap:0.35rem;align-items:center;flex-shrink:0;">${participants}</div>
</div>`; </div>`;
}).join(''); }).join('');
} catch(e) { list.innerHTML = ''; } } catch(e) { list.innerHTML = ''; }
@@ -954,7 +1041,7 @@
: '◉'; : '◉';
const bildRaw = bilderCarousel(p.bilder); const bildRaw = bilderCarousel(p.bilder);
const bildHtml = bildRaw const bildHtml = bildRaw
? `<div class="post-bild-wrap" data-post-id="${p.postId}">${bildRaw}<div class="post-bild-hover-icon">💬</div></div>` ? `<div class="post-bild-wrap" data-post-id="${p.postId}">${bildRaw}</div>`
: ''; : '';
const privacyLabel = p.isPublic ? '' : '<span style="font-size:0.72rem;color:var(--color-muted);margin-left:0.4rem;">🔒 privat</span>'; const privacyLabel = p.isPublic ? '' : '<span style="font-size:0.72rem;color:var(--color-muted);margin-left:0.4rem;">🔒 privat</span>';

View File

@@ -167,6 +167,33 @@
} }
.blind-hint-icon { font-size: 1.4rem; flex-shrink: 0; } .blind-hint-icon { font-size: 1.4rem; flex-shrink: 0; }
/* Bestätigungs-Modal */
.confirm-modal-bg {
display: none; position: fixed; inset: 0; z-index: 600;
align-items: center; justify-content: center;
}
.confirm-modal-bg.open { display: flex; }
.confirm-modal-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.6); }
.confirm-modal-box {
position: relative; background: var(--color-card);
border: 1px solid var(--color-secondary); border-radius: 12px;
padding: 1.75rem 1.5rem 1.5rem; max-width: 380px; width: 92%; z-index: 1;
display: flex; flex-direction: column; gap: 1rem;
}
.confirm-modal-title {
font-weight: 700; font-size: 1rem; padding-right: 1.5rem;
}
.confirm-modal-text {
font-size: 0.9rem; color: var(--color-muted); line-height: 1.5;
}
.confirm-modal-actions {
display: flex; gap: 0.6rem; justify-content: flex-end; margin-top: 0.25rem;
}
.confirm-modal-actions button { width: auto; padding: 0.6rem 1.3rem; font-size: 0.9rem; }
.confirm-modal-cancel { background: var(--color-secondary) !important; color: var(--color-text) !important; }
.confirm-modal-ok { background: #c0392b !important; }
.confirm-modal-ok:hover { background: #a93226 !important; }
/* Entsperrcode-Modal */ /* Entsperrcode-Modal */
.unlock-modal-bg { .unlock-modal-bg {
display: none; position: fixed; inset: 0; z-index: 500; display: none; position: fixed; inset: 0; z-index: 500;
@@ -214,6 +241,20 @@
</div> </div>
</div> </div>
<!-- Bestätigungs-Modal -->
<div class="confirm-modal-bg" id="confirmModal">
<div class="confirm-modal-overlay" onclick="confirmCancel()"></div>
<div class="confirm-modal-box">
<button onclick="confirmCancel()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;" title="Schließen"></button>
<div class="confirm-modal-title" id="confirmTitle"></div>
<div class="confirm-modal-text" id="confirmText"></div>
<div class="confirm-modal-actions">
<button class="confirm-modal-cancel" onclick="confirmCancel()">Abbrechen</button>
<button class="confirm-modal-ok" id="confirmOkBtn">Bestätigen</button>
</div>
</div>
</div>
<!-- Lockee-Einladungs-Dialog --> <!-- Lockee-Einladungs-Dialog -->
<div class="lockee-dialog-bg" id="lockeeInviteDialog"> <div class="lockee-dialog-bg" id="lockeeInviteDialog">
<div class="lockee-dialog-overlay" onclick="closeLockeeInviteDialog()"></div> <div class="lockee-dialog-overlay" onclick="closeLockeeInviteDialog()"></div>
@@ -475,9 +516,29 @@
renderSentPage(); renderSentPage();
} }
// ── Bestätigungs-Modal ──
let _confirmResolve = null;
function showConfirm(title, text) {
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmText').textContent = text;
document.getElementById('confirmModal').classList.add('open');
return new Promise(resolve => {
_confirmResolve = resolve;
document.getElementById('confirmOkBtn').onclick = () => { confirmClose(true); };
});
}
function confirmCancel() { confirmClose(false); }
function confirmClose(result) {
document.getElementById('confirmModal').classList.remove('open');
if (_confirmResolve) { _confirmResolve(result); _confirmResolve = null; }
}
// ── Aktionen: Empfangen ── // ── Aktionen: Empfangen ──
async function declineLockeeInvitation(token, btn) { async function declineLockeeInvitation(token, btn) {
if (!confirm('Bist du sicher, dass du diese Einladung ablehnen möchtest?')) return; if (!await showConfirm('Einladung ablehnen', 'Bist du sicher, dass du diese Einladung ablehnen möchtest?')) return;
btn.disabled = true; btn.disabled = true;
try { try {
const res = await fetch('/lockee/invitation/' + encodeURIComponent(token), { method: 'DELETE' }); const res = await fetch('/lockee/invitation/' + encodeURIComponent(token), { method: 'DELETE' });
@@ -487,7 +548,7 @@
} }
async function declineKhInvitation(token, btn) { async function declineKhInvitation(token, btn) {
if (!confirm('Bist du sicher, dass du diese Einladung ablehnen möchtest?')) return; if (!await showConfirm('Einladung ablehnen', 'Bist du sicher, dass du diese Einladung ablehnen möchtest?')) return;
btn.disabled = true; btn.disabled = true;
try { try {
const res = await fetch('/keyholder/invitations/mine/' + encodeURIComponent(token), { method: 'DELETE' }); const res = await fetch('/keyholder/invitations/mine/' + encodeURIComponent(token), { method: 'DELETE' });
@@ -498,10 +559,11 @@
// ── Aktionen: Gesendet ── // ── Aktionen: Gesendet ──
async function cancelSentInvitation(token, type, btn) { async function cancelSentInvitation(token, type, btn) {
const msg = type === 'lockee' const title = 'Einladung zurückziehen';
? 'Einladung zurückziehen? Das Lock wird gelöscht und der Lockee wird benachrichtigt.' const text = type === 'lockee'
: 'Keyholder-Einladung zurückziehen? Der Keyholder wird benachrichtigt.'; ? 'Das Lock wird gelöscht und der Lockee wird benachrichtigt.'
if (!confirm(msg)) return; : 'Der Keyholder wird benachrichtigt.';
if (!await showConfirm(title, text)) return;
btn.disabled = true; btn.disabled = true;
const url = type === 'lockee' const url = type === 'lockee'
? '/lockee/invitations/sent/' + encodeURIComponent(token) ? '/lockee/invitations/sent/' + encodeURIComponent(token)
@@ -634,7 +696,7 @@
async function declineLockeeInviteDialog() { async function declineLockeeInviteDialog() {
if (!activeDialogToken) return; if (!activeDialogToken) return;
if (!confirm('Bist du sicher, dass du diese Einladung ablehnen möchtest? Das Lock wird gelöscht.')) return; if (!await showConfirm('Einladung ablehnen', 'Bist du sicher, dass du diese Einladung ablehnen möchtest? Das Lock wird gelöscht.')) return;
const declineBtn = document.querySelector('.btn-decline'); const declineBtn = document.querySelector('.btn-decline');
declineBtn.disabled = true; declineBtn.disabled = true;
try { try {

View File

@@ -0,0 +1,968 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einstellungen XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.settings-page h1 {
font-size: 1.6rem;
margin-bottom: 1.5rem;
}
/* ── Accordion ── */
.settings-section {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
margin-bottom: 0.75rem;
overflow: hidden;
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 1rem 1.25rem;
cursor: pointer;
user-select: none;
transition: background 0.15s;
}
.settings-section-header:hover {
background: rgba(255,255,255,0.03);
}
.settings-section-title {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 1rem;
font-weight: 600;
}
.settings-section-arrow {
font-size: 0.75rem;
color: var(--color-muted);
transition: transform 0.2s;
}
.settings-section.open .settings-section-arrow {
transform: rotate(90deg);
}
.settings-section-body {
display: none;
padding: 0 1.25rem 1.25rem;
border-top: 1px solid var(--color-secondary);
}
.settings-section.open .settings-section-body {
display: block;
}
/* ── Einstellungs-Zeilen ── */
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.9rem 0;
}
.settings-row-info {
flex: 1;
}
.settings-row-label {
font-size: 0.95rem;
font-weight: 500;
margin-bottom: 0.15rem;
}
.settings-row-desc {
font-size: 0.8rem;
color: var(--color-muted);
}
.settings-row select {
width: auto;
min-width: 150px;
padding: 0.45rem 0.75rem;
margin: 0;
font-size: 0.9rem;
}
/* ── Benachrichtigungen Grid ── */
.notif-grid {
display: grid;
grid-template-columns: 1fr 5rem 5rem;
align-items: center;
}
.notif-grid-row {
display: contents;
}
.notif-grid-row > * {
padding: 0.75rem 0;
}
.notif-grid-row.notif-header > * {
padding-bottom: 0.4rem;
}
.notif-col-check {
display: flex;
justify-content: center;
align-items: center;
}
.notif-col-check input[type="checkbox"] {
width: 1.1rem;
height: 1.1rem;
accent-color: var(--color-primary, #c0392b);
cursor: pointer;
}
/* ── Speichern-Feedback (Toast) ── */
.save-toast {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%) translateY(2rem);
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-left: 3px solid var(--color-success, #4caf50);
border-radius: 8px;
padding: 0.6rem 1.25rem;
font-size: 0.88rem;
color: var(--color-success, #4caf50);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
z-index: 500;
white-space: nowrap;
}
.save-toast.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ── Spiel-Einstellungen Check-Items ── */
.spiel-check-group {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: 0.35rem;
}
.spiel-check-item {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: var(--color-secondary);
border: 1px solid transparent;
border-radius: 6px;
padding: 0.35rem 0.65rem;
cursor: pointer;
transition: border-color 0.15s;
user-select: none;
font-size: 0.88rem;
}
.spiel-check-item.is-checked { border-color: var(--color-primary); }
.spiel-check-item input {
accent-color: var(--color-primary);
width: auto;
cursor: pointer;
}
.spiel-subsection {
margin-bottom: 1.25rem;
}
.spiel-subsection-title {
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.75rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--color-secondary);
}
.spiel-field { margin-bottom: 1rem; }
.spiel-field > .settings-row-label { margin-bottom: 0.2rem; }
/* ── Modal ── */
.modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
z-index: 200;
align-items: center;
justify-content: center;
}
.modal-backdrop.visible { display: flex; }
.modal {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
padding: 2rem;
width: 100%;
max-width: 360px;
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
}
.modal h2 {
color: var(--color-primary);
font-size: 1.2rem;
margin-bottom: 1.25rem;
}
.modal-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.25rem;
}
.modal-actions button { flex: 1; margin-top: 0; }
/* ── Separator ── */
.settings-separator {
border: none;
border-top: 1px solid var(--color-secondary);
margin: 1.25rem 0 0;
}
/* ── Vorschau-Buttons ── */
.preview-row {
padding-top: 1.25rem;
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
align-items: center;
}
.preview-row span {
font-size: 0.85rem;
color: var(--color-muted);
flex-basis: 100%;
margin-bottom: 0.25rem;
}
.preview-row button {
width: auto;
padding: 0.5rem 1.1rem;
margin: 0;
font-size: 0.9rem;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h1>⚙️ Einstellungen</h1>
<!-- Grunddaten -->
<div class="settings-section" id="sec-grunddaten">
<div class="settings-section-header" onclick="toggleSection('sec-grunddaten')">
<span class="settings-section-title">👤 Grunddaten</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Nickname</div>
<div class="settings-row-desc" id="gd-name"></div>
</div>
<button onclick="openNameDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
</div>
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">E-Mail</div>
<div class="settings-row-desc" id="gd-email"></div>
</div>
<button onclick="openEmailDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
</div>
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Geburtsdatum</div>
<div class="settings-row-desc" id="gd-geburtsdatum"></div>
</div>
<button onclick="openGeburtsdatumDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
</div>
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label" style="color:var(--color-primary);">Konto löschen</div>
<div class="settings-row-desc">Alle Daten werden unwiderruflich gelöscht</div>
</div>
<button onclick="openDeleteDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;background:transparent;border:1px solid var(--color-secondary);color:var(--color-muted);">Löschen</button>
</div>
</div>
</div>
<!-- Spiel Einstellungen -->
<div class="settings-section" id="sec-spiel">
<div class="settings-section-header" onclick="toggleSection('sec-spiel')">
<span class="settings-section-title">🕹️ Spiel Einstellungen</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<!-- BDSM Game -->
<div class="spiel-subsection">
<div class="spiel-subsection-title">BDSM Game</div>
<div class="spiel-field">
<div class="settings-row-label">Spiele mit Geschlecht</div>
<div class="spiel-check-group" id="bdsm-spieltmit">
<label class="spiel-check-item"><input type="checkbox" value="WEIBLICH"> Weiblich</label>
<label class="spiel-check-item"><input type="checkbox" value="DIVERS"> Divers</label>
<label class="spiel-check-item"><input type="checkbox" value="MAENNLICH"> Männlich</label>
</div>
</div>
<div class="spiel-field">
<div class="settings-row-label">Meine Rollen</div>
<div class="spiel-check-group" id="bdsm-rollen">
<label class="spiel-check-item"><input type="checkbox" value="AUFGABE_AKTIV"> Aufgaben aktiv</label>
<label class="spiel-check-item"><input type="checkbox" value="AUFGABE_PASSIV"> Aufgaben passiv</label>
<label class="spiel-check-item"><input type="checkbox" value="BESTRAFUNG_AKTIV"> Bestrafungen aktiv</label>
<label class="spiel-check-item"><input type="checkbox" value="BESTRAFUNG_PASSIV"> Bestrafungen passiv</label>
</div>
</div>
<div class="spiel-field">
<div class="settings-row-label">Was ich einsetze</div>
<div class="spiel-check-group" id="bdsm-werkzeuge">
<label class="spiel-check-item"><input type="checkbox" value="MUND"> Mund</label>
<label class="spiel-check-item"><input type="checkbox" value="VAGINA"> Vagina</label>
<label class="spiel-check-item"><input type="checkbox" value="PENIS"> Penis</label>
<label class="spiel-check-item"><input type="checkbox" value="ANUS"> Anus</label>
<label class="spiel-check-item"><input type="checkbox" value="UMSCHNALLDILDO"> Umschnall-Dildo</label>
</div>
</div>
</div>
</div>
</div>
<!-- Benachrichtigungen -->
<div class="settings-section" id="sec-benachrichtigungen">
<div class="settings-section-header" onclick="toggleSection('sec-benachrichtigungen')">
<span class="settings-section-title">🔔 Benachrichtigungen</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<div class="notif-grid">
<!-- Header -->
<div class="notif-grid-row notif-header">
<div></div>
<div class="notif-col-check settings-row-label">In-App</div>
<div class="notif-col-check settings-row-label">E-Mail</div>
</div>
<!-- Einladungen -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Einladungen</div>
<div class="settings-row-desc">Einladungen zu Locks und Spielen, Annahmen und Ablehnungen</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-INVITATION-inApp" onchange="saveNotifications()">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-INVITATION-email" onchange="saveNotifications()">
</div>
</div>
<!-- Spielstatus -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Spielstatus</div>
<div class="settings-row-desc">Karten, Aufgaben, Verifikationen, Einfrierungen und andere Spielereignisse</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-GAME_STATE-inApp" onchange="saveNotifications()">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-GAME_STATE-email" onchange="saveNotifications()">
</div>
</div>
<!-- Notfall -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Notfall</div>
<div class="settings-row-desc">Notfall-Entsperrungen und dringende Meldungen</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-EMERGENCY-inApp" onchange="saveNotifications()">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-EMERGENCY-email" onchange="saveNotifications()">
</div>
</div>
<!-- Freundschaftsanfragen -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Freundschaftsanfragen</div>
<div class="settings-row-desc">Neue Freundschaftsanfragen von anderen Nutzern</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-FRIENDREQUEST-inApp" checked disabled
title="In-App-Benachrichtigungen für Freundschaftsanfragen sind immer aktiv">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-FRIENDREQUEST-email" onchange="saveNotifications()">
</div>
</div>
</div>
</div>
</div>
<!-- Datenschutz -->
<div class="settings-section" id="sec-datenschutz">
<div class="settings-section-header" onclick="toggleSection('sec-datenschutz')">
<span class="settings-section-title">🛡️ Datenschutz</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<!-- Grunddaten -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Grunddaten</div>
<div class="settings-row-desc">Alter, Größe, Gewicht, Geschlecht, Neigung, Beziehungsstatus, Beschreibung</div>
</div>
<select id="sv-grunddaten" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Galerie -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Galerie</div>
<div class="settings-row-desc">Fotos auf dem Profil</div>
</div>
<select id="sv-galerie" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Freunde -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Freundesliste</div>
<div class="settings-row-desc">Wer kann sehen, wer deine Freunde sind</div>
</div>
<select id="sv-freunde" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Feed -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Feed / Posts</div>
<div class="settings-row-desc">Posts auf dem Profil-Tab</div>
</div>
<select id="sv-feed" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Pinnwand -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Pinnwand</div>
<div class="settings-row-desc">Einträge auf der Pinnwand</div>
</div>
<select id="sv-pinnwand" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- XP -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">XP-Punkte</div>
<div class="settings-row-desc">Lockee-XP und Keyholder-XP</div>
</div>
<select id="sv-xp" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
<option value="NUR_ICH">Nur ich</option>
</select>
</div>
<!-- Lock-Historie -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Lock-Historie</div>
<div class="settings-row-desc">Abgeschlossene Locks und Keyholder-Aktivitäten</div>
</div>
<select id="sv-lockhistorie" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
<option value="NUR_ICH">Nur ich</option>
</select>
</div>
<hr class="settings-separator">
<!-- Vorschau -->
<div class="preview-row">
<span>Profil-Vorschau wie sieht mein Profil für andere aus?</span>
<button onclick="openPreview('FREUND')">👥 Vorschau als Freund</button>
<button onclick="openPreview('UNBEKANNT')" style="background:var(--color-secondary);color:var(--color-text);">👤 Vorschau als Fremder</button>
</div>
</div>
</div>
</div>
</div>
<!-- Nickname Modal -->
<div class="modal-backdrop" id="nameModal">
<div class="modal">
<h2>Nickname ändern</h2>
<label for="newName">Neuer Nickname</label>
<input type="text" id="newName" placeholder="Neuer Name" autocomplete="off">
<div class="message" id="nameMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeNameDialog()">Abbrechen</button>
<button id="nameConfirmBtn" onclick="saveName()">Speichern</button>
</div>
</div>
</div>
<!-- Geburtsdatum Modal -->
<div class="modal-backdrop" id="geburtsdatumModal">
<div class="modal">
<h2>Geburtsdatum ändern</h2>
<label for="newGeburtsdatum">Neues Geburtsdatum</label>
<input type="date" id="newGeburtsdatum" autocomplete="bday">
<div class="message" id="geburtsdatumMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeGeburtsdatumDialog()">Abbrechen</button>
<button id="geburtsdatumConfirmBtn" onclick="saveGeburtsdatum()">Speichern</button>
</div>
</div>
</div>
<!-- E-Mail Modal -->
<div class="modal-backdrop" id="emailModal">
<div class="modal">
<h2>E-Mail-Adresse ändern</h2>
<label for="newEmail">Neue E-Mail-Adresse</label>
<input type="email" id="newEmail" placeholder="neue@email.de" autocomplete="off">
<div class="message" id="emailMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeEmailDialog()">Abbrechen</button>
<button id="emailConfirmBtn" onclick="requestEmailChange()">Bestätigungsmail senden</button>
</div>
</div>
</div>
<!-- Konto löschen Modal -->
<div class="modal-backdrop" id="deleteModal">
<div class="modal">
<h2>Konto löschen</h2>
<p style="color:var(--color-muted);font-size:0.9rem;margin-bottom:0.75rem;">
Dein Konto sowie alle gespeicherten Aufgaben und Toys werden unwiderruflich gelöscht.
</p>
<p style="color:var(--color-primary);font-size:0.85rem;">Dieser Vorgang kann nicht rückgängig gemacht werden.</p>
<div class="message" id="deleteMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeDeleteDialog()">Abbrechen</button>
<button id="deleteConfirmBtn" onclick="deleteAccount()" style="background:#c0392b;">Konto löschen</button>
</div>
</div>
</div>
<div class="save-toast" id="saveToast">✓ Gespeichert</div>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
let myUserId = null;
let toastTimer = null;
function toggleSection(id) {
const target = document.getElementById(id);
const isOpen = target.classList.contains('open');
document.querySelectorAll('.settings-section').forEach(s => s.classList.remove('open'));
if (!isOpen) target.classList.add('open');
}
// ── Grunddaten laden ──
async function loadGrunddaten() {
const res = await fetch('/login/me');
if (res.status === 401) { window.location.href = '/login.html'; return; }
if (!res.ok) return;
const user = await res.json();
myUserId = user.userId;
document.getElementById('gd-name').textContent = user.name || '—';
document.getElementById('gd-email').textContent = user.email || '—';
document.getElementById('gd-geburtsdatum').textContent = formatGeburtsdatum(user.geburtsdatum);
}
function formatGeburtsdatum(iso) {
if (!iso) return '—';
const [y, m, d] = iso.split('-');
const today = new Date();
const birth = new Date(iso);
const age = today.getFullYear() - birth.getFullYear()
- (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
return `${d}.${m}.${y} (${age} Jahre)`;
}
// ── Geburtsdatum Dialog ──
function openGeburtsdatumDialog() {
document.getElementById('newGeburtsdatum').value = '';
hideModalMessage('geburtsdatumMessage');
document.getElementById('geburtsdatumModal').classList.add('visible');
document.getElementById('newGeburtsdatum').focus();
}
function closeGeburtsdatumDialog() {
document.getElementById('geburtsdatumModal').classList.remove('visible');
}
async function saveGeburtsdatum() {
const val = document.getElementById('newGeburtsdatum').value;
if (!val) { showModalMessage('geburtsdatumMessage', 'Bitte ein Datum eingeben.', 'error'); return; }
const today = new Date();
const birth = new Date(val);
const age = today.getFullYear() - birth.getFullYear()
- (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
if (age < 18) {
showModalMessage('geburtsdatumMessage', 'Du musst mindestens 18 Jahre alt sein.', 'error');
return;
}
const btn = document.getElementById('geburtsdatumConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
hideModalMessage('geburtsdatumMessage');
try {
const res = await fetch('/user/me/geburtsdatum', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ geburtsdatum: val })
});
if (res.ok) {
document.getElementById('gd-geburtsdatum').textContent = formatGeburtsdatum(val);
closeGeburtsdatumDialog();
showToast();
} else if (res.status === 422) {
showModalMessage('geburtsdatumMessage', 'Du musst mindestens 18 Jahre alt sein.', 'error');
} else {
showModalMessage('geburtsdatumMessage', `Fehler: HTTP ${res.status}`, 'error');
}
} catch (err) {
showModalMessage('geburtsdatumMessage', 'Server nicht erreichbar.', 'error');
console.error(err);
} finally {
btn.disabled = false; btn.textContent = 'Speichern';
}
}
// ── Nickname Dialog ──
function openNameDialog() {
document.getElementById('newName').value = '';
hideModalMessage('nameMessage');
document.getElementById('nameModal').classList.add('visible');
document.getElementById('newName').focus();
}
function closeNameDialog() {
document.getElementById('nameModal').classList.remove('visible');
}
async function saveName() {
const newName = document.getElementById('newName').value.trim();
if (!newName) { showModalMessage('nameMessage', 'Bitte einen Namen eingeben.', 'error'); return; }
const btn = document.getElementById('nameConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
hideModalMessage('nameMessage');
try {
const res = await fetch('/user/me/name', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName })
});
if (res.ok) {
document.getElementById('gd-name').textContent = newName;
closeNameDialog();
showToast();
} else if (res.status === 409) {
showModalMessage('nameMessage', 'Dieser Nickname ist bereits vergeben.', 'error');
} else {
showModalMessage('nameMessage', `Fehler: HTTP ${res.status}`, 'error');
}
} catch (err) {
showModalMessage('nameMessage', 'Server nicht erreichbar.', 'error');
console.error(err);
} finally {
btn.disabled = false; btn.textContent = 'Speichern';
}
}
// ── E-Mail Dialog ──
function openEmailDialog() {
document.getElementById('newEmail').value = '';
hideModalMessage('emailMessage');
document.getElementById('emailModal').classList.add('visible');
document.getElementById('newEmail').focus();
}
function closeEmailDialog() {
document.getElementById('emailModal').classList.remove('visible');
}
async function requestEmailChange() {
const newEmail = document.getElementById('newEmail').value.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
showModalMessage('emailMessage', 'Bitte eine gültige E-Mail-Adresse eingeben.', 'error');
return;
}
const btn = document.getElementById('emailConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gesendet…';
hideModalMessage('emailMessage');
try {
const res = await fetch('/email-change', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ newEmail })
});
if (res.status === 202) {
showModalMessage('emailMessage',
'Bestätigungsmail wurde an die neue Adresse gesendet. Bitte bestätige die Änderung über den Link in der E-Mail.',
'success');
btn.disabled = true; btn.textContent = 'Gesendet';
} else if (res.status === 409) {
showModalMessage('emailMessage', 'Diese E-Mail-Adresse ist bereits vergeben.', 'error');
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
} else {
showModalMessage('emailMessage', `Fehler: HTTP ${res.status}`, 'error');
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
}
} catch (err) {
showModalMessage('emailMessage', 'Server nicht erreichbar.', 'error');
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
console.error(err);
}
}
// ── Konto löschen Dialog ──
function openDeleteDialog() {
hideModalMessage('deleteMessage');
document.getElementById('deleteConfirmBtn').disabled = false;
document.getElementById('deleteConfirmBtn').textContent = 'Konto löschen';
document.getElementById('deleteModal').classList.add('visible');
}
function closeDeleteDialog() {
document.getElementById('deleteModal').classList.remove('visible');
}
async function deleteAccount() {
const btn = document.getElementById('deleteConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gelöscht…';
hideModalMessage('deleteMessage');
try {
const res = await fetch('/user/me', { method: 'DELETE' });
if (res.ok) {
window.location.href = '/login.html?accountDeleted=1';
} else {
showModalMessage('deleteMessage', `Fehler: HTTP ${res.status}`, 'error');
btn.disabled = false; btn.textContent = 'Konto löschen';
}
} catch (err) {
showModalMessage('deleteMessage', 'Server nicht erreichbar.', 'error');
btn.disabled = false; btn.textContent = 'Konto löschen';
console.error(err);
}
}
function showModalMessage(id, text, type) {
const el = document.getElementById(id);
el.textContent = text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideModalMessage(id) {
document.getElementById(id).style.display = 'none';
}
// Modal-Backdrop-Klick schließt Modals
['nameModal','geburtsdatumModal','emailModal','deleteModal'].forEach(id => {
document.getElementById(id).addEventListener('click', e => {
if (e.target === document.getElementById(id)) document.getElementById(id).classList.remove('visible');
});
});
document.getElementById('newName').addEventListener('keydown', e => { if (e.key === 'Enter') saveName(); });
document.getElementById('newEmail').addEventListener('keydown', e => { if (e.key === 'Enter') requestEmailChange(); });
// ── Datenschutz laden ──
async function loadPrivacy() {
const meRes = await fetch('/login/me');
if (!meRes.ok) return;
const me = await meRes.json();
myUserId = me.userId;
const profRes = await fetch('/social/users/' + myUserId);
if (!profRes.ok) return;
const profile = await profRes.json();
setValue('sv-grunddaten', profile.sichtbarkeitGrunddaten || 'ALLE');
setValue('sv-galerie', profile.sichtbarkeitGalerie || 'ALLE');
setValue('sv-freunde', profile.sichtbarkeitFreunde || 'ALLE');
setValue('sv-feed', profile.sichtbarkeitFeed || 'ALLE');
setValue('sv-pinnwand', profile.sichtbarkeitPinnwand || 'ALLE');
setValue('sv-xp', profile.sichtbarkeitXp || 'ALLE');
setValue('sv-lockhistorie', profile.sichtbarkeitLockhistorie || 'ALLE');
}
function setValue(id, value) {
const el = document.getElementById(id);
if (el) el.value = value;
}
async function doSave() {
const body = {
sichtbarkeitGrunddaten: document.getElementById('sv-grunddaten').value,
sichtbarkeitGalerie: document.getElementById('sv-galerie').value,
sichtbarkeitFreunde: document.getElementById('sv-freunde').value,
sichtbarkeitFeed: document.getElementById('sv-feed').value,
sichtbarkeitPinnwand: document.getElementById('sv-pinnwand').value,
sichtbarkeitXp: document.getElementById('sv-xp').value,
sichtbarkeitLockhistorie: document.getElementById('sv-lockhistorie').value,
};
const res = await fetch('/user/me/privacy', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) showToast();
}
function showToast() {
const t = document.getElementById('saveToast');
t.classList.add('visible');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('visible'), 2200);
}
function openPreview(mode) {
if (!myUserId) return;
window.open('/benutzer.html?userId=' + myUserId + '&preview=' + mode, '_blank');
}
// ── Benachrichtigungen laden ──
async function loadNotifications() {
const res = await fetch('/user/me/notifications');
if (!res.ok) return;
const data = await res.json();
for (const cause of ['INVITATION', 'GAME_STATE', 'EMERGENCY']) {
const pref = data[cause] || { inApp: true, email: false };
const inApp = document.getElementById('notif-' + cause + '-inApp');
const email = document.getElementById('notif-' + cause + '-email');
if (inApp) inApp.checked = pref.inApp;
if (email) email.checked = pref.email;
}
}
async function saveNotifications() {
const body = {};
for (const cause of ['INVITATION', 'GAME_STATE', 'EMERGENCY']) {
const inApp = document.getElementById('notif-' + cause + '-inApp');
const email = document.getElementById('notif-' + cause + '-email');
body[cause] = {
inApp: inApp ? inApp.checked : true,
email: email ? email.checked : false
};
}
const res = await fetch('/user/me/notifications', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) showToast();
}
// Hash-Navigation: Sektion anhand URL-Fragment öffnen
function openSectionFromHash() {
const hash = window.location.hash.replace('#', '');
if (hash) {
const el = document.getElementById(hash);
if (el) el.classList.add('open');
}
}
// ── Spiel Einstellungen BDSM ──
function applyCheckItems(groupId, values) {
document.querySelectorAll(`#${groupId} .spiel-check-item input`).forEach(cb => {
const checked = values.includes(cb.value);
cb.checked = checked;
cb.closest('.spiel-check-item').classList.toggle('is-checked', checked);
});
}
document.querySelectorAll('.spiel-check-item input').forEach(cb => {
cb.addEventListener('change', () => {
cb.closest('.spiel-check-item').classList.toggle('is-checked', cb.checked);
saveBdsmDefaults();
});
});
async function loadBdsmDefaults() {
const res = await fetch('/user/me/bdsm-defaults');
if (!res.ok) return;
const d = await res.json();
applyCheckItems('bdsm-spieltmit', d.spieltMit || []);
applyCheckItems('bdsm-rollen', d.rollen || []);
applyCheckItems('bdsm-werkzeuge', d.werkzeuge || []);
}
async function saveBdsmDefaults() {
const get = groupId => [...document.querySelectorAll(`#${groupId} input:checked`)].map(cb => cb.value);
await fetch('/user/me/bdsm-defaults', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
spieltMit: get('bdsm-spieltmit'),
rollen: get('bdsm-rollen'),
werkzeuge: get('bdsm-werkzeuge'),
})
});
showToast();
}
loadGrunddaten();
loadPrivacy();
loadNotifications();
loadBdsmDefaults();
openSectionFromHash();
</script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More