Ä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(ls -lah /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/*.java)",
"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
host=Mario-Linux
process-id=148721
process-id=26624
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 org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204)
... 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
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
1067882983.index
3547251881.index
1022297761.index
2655170954.index
176453541.index
677104696.index
341080888.index
774576701.index
4134502745.index
1780956574.index
369020172.index
41199409.index
134995224.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
2127778675.index
2389383899.index
2701419231.index
2519831052.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
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
2032345814.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
352173590.index
766439048.index
41199409.index
2900482015.index
3952767374.index
773718761.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
3424266581.index
2247053514.index
1765772496.index
3514351073.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.Setter"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.Test"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.KeyholderCardLock"/>
</qualifiedTypeNameHistroy>

View File

@@ -24,6 +24,10 @@
<section name="RenameInformationPopup">
</section>
<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 name="org.eclipse.jdt.internal.ui.dialogs.OpenTypeSelectionDialog2">
<item key="ShowStatusLine" value="true"/>
@@ -68,4 +72,13 @@
<section name="BuildPathsPropertyPage">
<item key="pageIndex" value="3"/>
</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>

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: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-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_Y_ORIGIN" value="20"/>
<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|"/>
</section>
<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.platform=4.39.0.v20260226-0420

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,251 +1,265 @@
package de.oaa.xxx.session;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.session.aufgaben.Aufgabe;
import de.oaa.xxx.session.aufgaben.AufgabenList;
import de.oaa.xxx.session.aufgaben.Sperre;
import de.oaa.xxx.session.aufgaben.Strafe;
import de.oaa.xxx.session.entity.SessionEntity;
import de.oaa.xxx.session.sperre.SperreCallback;
import de.oaa.xxx.session.sperre.SperrenVerlaengernCallback;
public class SessionDurchfuehren {
private final AufgabenList aufgabenList;
private final List<Mitspieler> mitspieler = new ArrayList<>();
private final List<AktiveSperre> aktiveSperren = new ArrayList<>();
private final Integer wahrscheinlichkeitSperre;
private final Integer wahrscheinlichkeitStrafe;
private int aufgabenProLevel;
private int level;
private int aufgabenAufAktuellemLevel;
public SessionDurchfuehren(SessionEntity entity) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
aufgabenList = objectMapper.readValue(entity.getAufgaben(), AufgabenList.class);
entity.getMitspieler().forEach(mitspielerEntity -> mitspieler.add(mitspielerEntity.toMitspieler()));
entity.getAktiveSperren().forEach(sperreEntity -> aktiveSperren.add(sperreEntity.toSperre(mitspieler)));
wahrscheinlichkeitSperre = entity.getWahrscheinlichkeitSperre();
wahrscheinlichkeitStrafe = entity.getWahrscheinlichkeitStrafe();
this.aufgabenProLevel = entity.getAufgabenProLevel() != null ? entity.getAufgabenProLevel() : 5;
this.level = entity.getLevel() != null ? entity.getLevel() : 1;
this.aufgabenAufAktuellemLevel = entity.getAufgabenAufAktuellemLevel() != null ? entity.getAufgabenAufAktuellemLevel() : 0;
}
public AufgabeAnzeige getNext() {
checkLevel();
if (level == 6) {
return null;
}
AufgabeAnzeige anzeige = null;
int nextInt = new Random().nextInt(1, 100);
if (nextInt == 1) {
anzeige = findUltimativeStrafe();
} else if (nextInt == 2) {
anzeige = findSperreVerlaengern();
} else if (nextInt > wahrscheinlichkeitSperre + wahrscheinlichkeitStrafe + 2) {
anzeige = findeAufgabe();
} else if (nextInt > wahrscheinlichkeitSperre + 2) {
anzeige = findeStrafe();
} else {
anzeige = findeSperre();
}
if (anzeige == null) {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_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.";
anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv != null ? aktiv.getName() : "");
anzeige.setAufgabeText(getAnzeigeText(text, aktiv != null ? aktiv.getName() : "?", passiv != null ? passiv.getName() : "?"));
anzeige.setTimer(120);
}
return anzeige;
}
public void backToLvl5() {
this.level = 5;
this.aufgabenAufAktuellemLevel = 0;
}
public List<AufgabeAnzeige> getFinisher() {
var list = new ArrayList<AufgabeAnzeige>();
List.of(GeschlechtEnum.WEIBLICH, GeschlechtEnum.DIVERS, GeschlechtEnum.MAENNLICH).forEach(geschlecht -> {
mitspieler.stream().filter(m -> geschlecht == m.getGeschlecht()).toList().forEach(cumming -> {
var partner = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, cumming);
var finishers = aufgabenList.getFinisher().stream()
.filter(finisher -> geschlecht == finisher.getGeschlecht())
.toList();
if (!finishers.isEmpty()) {
var aufgabe = finishers.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(cumming.getName());
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(),
cumming.getName(), partner != null ? partner.getName() : ""));
list.add(anzeige);
}
});
});
return list;
}
private void checkLevel() {
if (++aufgabenAufAktuellemLevel >= 1 + aufgabenProLevel) {
aufgabenAufAktuellemLevel = 0;
level++;
}
}
private AufgabeAnzeige findUltimativeStrafe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
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();
anzeige.setNameAktiverMitspieler(aktiv.getName());
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setTimer(new Random().nextInt(1800, 7200));
return anzeige;
}
}
return findeStrafe();
}
private AufgabeAnzeige findSperreVerlaengern() {
if (!aktiveSperren.isEmpty()) {
AktiveSperre sperre = aktiveSperren.get(new Random().nextInt(aktiveSperren.size()));
Mitspieler passiv = sperre.getMitspieler();
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV, passiv);
if (aktiv != null) {
String text = "{AKTIV}, du entscheidest. Sollen alle bestehenden Zeitstrafen von {PASSIV} verlängert werden...?";
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setNameAktiverMitspieler(aktiv.getName());
SperrenVerlaengernCallback callback = new SperrenVerlaengernCallback();
callback.setFaktor(new Random().nextInt(2, 4));
callback.setSpielerId(passiv.getId());
anzeige.setCallback(callback);
return anzeige;
}
}
return findeSperre();
}
private AufgabeAnzeige findeAufgabe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
if (passiv != null) {
List<Aufgabe> list = aufgabenList.getAufgaben().stream()
.filter(aufgabe -> aufgabe.isAufgabePassend(level, aktiv, passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Aufgabe aufgabe = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(), aktiv.getName(), passiv.getName()));
if (aufgabe.getSekundenVon() != null) {
if (aufgabe.getSekundenBis() != null) {
anzeige.setTimer(new Random().nextInt(aufgabe.getSekundenVon(), aufgabe.getSekundenBis()));
} else {
anzeige.setTimer(aufgabe.getSekundenVon());
}
}
return anzeige;
}
}
}
return findeStrafe();
}
private AufgabeAnzeige findeStrafe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
List<Strafe> list = aufgabenList.getStrafen().stream()
.filter(strafe -> strafe.isAufgabePassend(level, aktiv, passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Strafe strafe = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
anzeige.setAufgabeText(getAnzeigeText(strafe.getText(), aktiv.getName(), passiv.getName()));
if (strafe.getSekundenVon() != null) {
if (strafe.getSekundenBis() != null) {
anzeige.setTimer(new Random().nextInt(strafe.getSekundenVon(), strafe.getSekundenBis()));
} else {
anzeige.setTimer(strafe.getSekundenVon());
}
}
return anzeige;
}
}
}
return findeSperre();
}
private AufgabeAnzeige findeSperre() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
List<Sperre> list = aufgabenList.getSperren().stream()
.filter(sperre -> sperre.isAufgabePassend(passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Sperre sperre = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
anzeige.setAufgabeText(getAnzeigeText(sperre.getText(), aktiv.getName(), passiv.getName()));
SperreCallback callback = new SperreCallback();
callback.setSperreId(sperre.getSperreId());
callback.setSpielerId(passiv.getId());
callback.setReleaseText(getAnzeigeText(sperre.getReleaseText(), aktiv.getName(), passiv.getName()));
anzeige.setCallback(callback);
return anzeige;
}
}
}
return null;
}
private String getAnzeigeText(String textMitPlatzhaltern, String nameAktiv, String namePassiv) {
return textMitPlatzhaltern.replace("{AKTIV}", nameAktiv).replace("{PASSIV}", namePassiv);
}
private Mitspieler findeMitspielerMitRolle(RolleEnum rolle) {
List<Mitspieler> list = mitspieler.stream()
.filter(m -> m.getRollen().contains(rolle))
.toList();
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size()));
}
private Mitspieler findeMitspielerMitRolle(RolleEnum rolle, Mitspieler gegenspieler) {
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;
}
}
package de.oaa.xxx.games.bdsm;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.games.bdsm.aufgaben.Aufgabe;
import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
import de.oaa.xxx.games.bdsm.aufgaben.Sperre;
import de.oaa.xxx.games.bdsm.aufgaben.Strafe;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.games.bdsm.sperre.SperreCallback;
import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback;
public class BdsmGameDurchfuehren {
private final AufgabenList aufgabenList;
private final List<Mitspieler> mitspieler = new ArrayList<>();
private final List<AktiveSperre> aktiveSperren = new ArrayList<>();
private final Integer wahrscheinlichkeitSperre;
private final Integer wahrscheinlichkeitStrafe;
private int aufgabenProLevel;
private int level;
private int aufgabenAufAktuellemLevel;
public BdsmGameDurchfuehren(BdsmGameEntity entity) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
aufgabenList = objectMapper.readValue(entity.getAufgaben(), AufgabenList.class);
entity.getMitspieler().forEach(mitspielerEntity -> mitspieler.add(mitspielerEntity.toMitspieler()));
entity.getAktiveSperren().forEach(sperreEntity -> aktiveSperren.add(sperreEntity.toSperre(mitspieler)));
wahrscheinlichkeitSperre = entity.getWahrscheinlichkeitSperre();
wahrscheinlichkeitStrafe = entity.getWahrscheinlichkeitStrafe();
this.aufgabenProLevel = entity.getAufgabenProLevel() != null ? entity.getAufgabenProLevel() : 5;
this.level = entity.getLevel() != null ? entity.getLevel() : 1;
this.aufgabenAufAktuellemLevel = entity.getAufgabenAufAktuellemLevel() != null ? entity.getAufgabenAufAktuellemLevel() : 0;
}
public AufgabeAnzeige getNext() {
checkLevel();
if (level == 6) {
return null;
}
AufgabeAnzeige anzeige = null;
int nextInt = new Random().nextInt(1, 100);
if (nextInt == 1) {
anzeige = findUltimativeStrafe();
} else if (nextInt == 2) {
anzeige = findSperreVerlaengern();
} else if (nextInt > wahrscheinlichkeitSperre + wahrscheinlichkeitStrafe + 2) {
anzeige = findeAufgabe();
} else if (nextInt > wahrscheinlichkeitSperre + 2) {
anzeige = findeStrafe();
} else {
anzeige = findeSperre();
}
if (anzeige == null) {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_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.";
anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv != null ? aktiv.getName() : "");
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(text, aktiv != null ? aktiv.getName() : "?", passiv != null ? passiv.getName() : "?"));
anzeige.setTimer(120);
}
return anzeige;
}
public void backToLvl5() {
this.level = 5;
this.aufgabenAufAktuellemLevel = 0;
}
public List<AufgabeAnzeige> getFinisher() {
var list = new ArrayList<AufgabeAnzeige>();
List.of(GeschlechtEnum.WEIBLICH, GeschlechtEnum.DIVERS, GeschlechtEnum.MAENNLICH).forEach(geschlecht -> {
mitspieler.stream().filter(m -> geschlecht == m.getGeschlecht()).toList().forEach(cumming -> {
var partner = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, cumming);
var finishers = aufgabenList.getFinisher().stream()
.filter(finisher -> geschlecht == finisher.getGeschlecht())
.toList();
if (!finishers.isEmpty()) {
var aufgabe = finishers.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(cumming.getName());
setMitspielerInfo(anzeige, cumming);
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(),
cumming.getName(), partner != null ? partner.getName() : ""));
list.add(anzeige);
}
});
});
return list;
}
private void checkLevel() {
if (++aufgabenAufAktuellemLevel >= 1 + aufgabenProLevel) {
aufgabenAufAktuellemLevel = 0;
level++;
}
}
private void setMitspielerInfo(AufgabeAnzeige anzeige, Mitspieler aktiv) {
if (aktiv != null) {
anzeige.setMitspielerId(aktiv.getId());
anzeige.setEigenesGeraet(aktiv.isEigenesGeraet());
}
}
private AufgabeAnzeige findUltimativeStrafe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
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();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setTimer(new Random().nextInt(1800, 7200));
return anzeige;
}
}
return findeStrafe();
}
private AufgabeAnzeige findSperreVerlaengern() {
if (!aktiveSperren.isEmpty()) {
AktiveSperre sperre = aktiveSperren.get(new Random().nextInt(aktiveSperren.size()));
Mitspieler passiv = sperre.getMitspieler();
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV, passiv);
if (aktiv != null) {
String text = "{AKTIV}, du entscheidest. Sollen alle bestehenden Zeitstrafen von {PASSIV} verlängert werden...?";
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
SperrenVerlaengernCallback callback = new SperrenVerlaengernCallback();
callback.setFaktor(new Random().nextInt(2, 4));
callback.setSpielerId(passiv.getId());
anzeige.setCallback(callback);
return anzeige;
}
}
return findeSperre();
}
private AufgabeAnzeige findeAufgabe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
if (passiv != null) {
List<Aufgabe> list = aufgabenList.getAufgaben().stream()
.filter(aufgabe -> aufgabe.isAufgabePassend(level, aktiv, passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Aufgabe aufgabe = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(), aktiv.getName(), passiv.getName()));
if (aufgabe.getSekundenVon() != null) {
if (aufgabe.getSekundenBis() != null) {
anzeige.setTimer(new Random().nextInt(aufgabe.getSekundenVon(), aufgabe.getSekundenBis()));
} else {
anzeige.setTimer(aufgabe.getSekundenVon());
}
}
return anzeige;
}
}
}
return findeStrafe();
}
private AufgabeAnzeige findeStrafe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
List<Strafe> list = aufgabenList.getStrafen().stream()
.filter(strafe -> strafe.isAufgabePassend(level, aktiv, passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Strafe strafe = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(strafe.getText(), aktiv.getName(), passiv.getName()));
if (strafe.getSekundenVon() != null) {
if (strafe.getSekundenBis() != null) {
anzeige.setTimer(new Random().nextInt(strafe.getSekundenVon(), strafe.getSekundenBis()));
} else {
anzeige.setTimer(strafe.getSekundenVon());
}
}
return anzeige;
}
}
}
return findeSperre();
}
private AufgabeAnzeige findeSperre() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
List<Sperre> list = aufgabenList.getSperren().stream()
.filter(sperre -> sperre.isAufgabePassend(passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Sperre sperre = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(sperre.getText(), aktiv.getName(), passiv.getName()));
SperreCallback callback = new SperreCallback();
callback.setSperreId(sperre.getSperreId());
callback.setSpielerId(passiv.getId());
callback.setReleaseText(getAnzeigeText(sperre.getReleaseText(), aktiv.getName(), passiv.getName()));
anzeige.setCallback(callback);
return anzeige;
}
}
}
return null;
}
private String getAnzeigeText(String textMitPlatzhaltern, String nameAktiv, String namePassiv) {
return textMitPlatzhaltern.replace("{AKTIV}", nameAktiv).replace("{PASSIV}", namePassiv);
}
private Mitspieler findeMitspielerMitRolle(RolleEnum rolle) {
List<Mitspieler> list = mitspieler.stream()
.filter(m -> m.getRollen().contains(rolle))
.toList();
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size()));
}
private Mitspieler findeMitspielerMitRolle(RolleEnum rolle, Mitspieler gegenspieler) {
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;

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session;
package de.oaa.xxx.games.bdsm;
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.session.Werkzeug;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter;
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.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.session.Werkzeug;
import de.oaa.xxx.games.bdsm.GeschlechtEnum;
import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter;
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.session.Werkzeug;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter;
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.session.Werkzeug;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter;
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 de.oaa.xxx.session.AufgabeAnzeige;
import de.oaa.xxx.session.Mitspieler;
import de.oaa.xxx.session.Session;
import de.oaa.xxx.session.SessionDurchfuehren;
import de.oaa.xxx.session.aufgaben.AufgabenList;
import de.oaa.xxx.session.entity.MitspielerEntity;
import de.oaa.xxx.session.entity.SessionEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository;
import de.oaa.xxx.session.repository.MitspielerRepository;
import de.oaa.xxx.session.repository.SessionRepository;
import de.oaa.xxx.games.bdsm.AufgabeAnzeige;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.BdsmGame;
import de.oaa.xxx.games.bdsm.BdsmGameDurchfuehren;
import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
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 org.slf4j.Logger;
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.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 org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.security.Principal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/session")
@RequestMapping("/bdsm")
@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 AktiveSperreRepository aktiveSperreRepository;
private final UserRepository userRepository;
private final GameHistoryRepository gameHistoryRepository;
private final BdsmEinladungRepository einladungRepository;
private final ObjectMapper objectMapper;
public SessionController(SessionRepository sessionRepository, MitspielerRepository mitspielerRepository,
public BdsmGameController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository,
AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository,
GameHistoryRepository gameHistoryRepository, BdsmEinladungRepository einladungRepository,
ObjectMapper objectMapper) {
this.sessionRepository = sessionRepository;
this.mitspielerRepository = mitspielerRepository;
this.aktiveSperreRepository = aktiveSperreRepository;
this.userRepository = userRepository;
this.gameHistoryRepository = gameHistoryRepository;
this.einladungRepository = einladungRepository;
this.objectMapper = objectMapper;
}
@GetMapping("/{sessionId}")
public ResponseEntity<Session> getBySessionId(@PathVariable UUID sessionId) {
public ResponseEntity<BdsmGame> getBySessionId(@PathVariable UUID sessionId) {
return sessionRepository.findById(sessionId)
.map(entity -> ResponseEntity.ok(toSession(entity)))
.orElse(ResponseEntity.noContent().build());
}
@GetMapping
public ResponseEntity<Session> getByUserId(@RequestParam UUID userId) {
public ResponseEntity<BdsmGame> getByUserId(@RequestParam UUID userId) {
return sessionRepository.findByUserId(userId)
.map(entity -> ResponseEntity.ok(toSession(entity)))
.orElse(ResponseEntity.noContent().build());
}
@PostMapping
public ResponseEntity<Void> create(@RequestBody Session session) {
public ResponseEntity<Void> create(@RequestBody BdsmGame session) {
String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
UUID userId = userRepository.findByEmail(email).map(u -> u.getUserId()).orElse(null);
if (userId == null) return ResponseEntity.status(401).build();
SessionEntity entity = new SessionEntity();
BdsmGameEntity entity = new BdsmGameEntity();
entity.setSessionId(UUID.randomUUID());
entity.setUserId(userId);
entity.setAufgabenAufAktuellemLevel(0);
@@ -85,8 +101,16 @@ public class SessionController {
entity.setWahrscheinlichkeitStrafe(session.getWahrscheinlichkeitStrafe() != null ? session.getWahrscheinlichkeitStrafe() : 10);
entity.setZeitfaktorZeitstrafen(session.getZeitfaktorZeitstrafen() != null ? session.getZeitfaktorZeitstrafen() : 1.0);
entity.setLevel(1);
entity.setSetupId(session.getSetupId());
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.getWahrscheinlichkeitStrafe(), entity.getWahrscheinlichkeitSperre(),
entity.getZeitfaktorZeitstrafen());
@@ -96,9 +120,20 @@ public class SessionController {
}
@DeleteMapping
public ResponseEntity<Void> deleteSession(@RequestBody Session session) {
public ResponseEntity<Void> deleteSession(@RequestBody BdsmGame session) {
return sessionRepository.findById(session.getSessionId())
.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());
mitspielerRepository.deleteAll(entity.getMitspieler());
sessionRepository.delete(entity);
@@ -114,7 +149,7 @@ public class SessionController {
return ResponseEntity.badRequest().build();
}
String aufgaben = objectMapper.writeValueAsString(list);
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) {
return ResponseEntity.badRequest().build();
}
@@ -130,12 +165,12 @@ public class SessionController {
@GetMapping("/{sessionId}/aufgaben/next")
public ResponseEntity<AufgabeAnzeige> getNextAufgabe(@PathVariable UUID sessionId) {
try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) {
return ResponseEntity.badRequest().build();
}
session.setLetzteAktivitaet(LocalDateTime.now());
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session);
BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
AufgabeAnzeige next = durchfuehren.getNext();
session.setLevel(durchfuehren.getLevel());
session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel());
@@ -165,7 +200,7 @@ public class SessionController {
|| mitspieler.getVerfuegbareWerkzeuge() == null || mitspieler.getVerfuegbareWerkzeuge().isEmpty()) {
return ResponseEntity.badRequest().build();
}
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) {
return ResponseEntity.badRequest().build();
}
@@ -176,6 +211,8 @@ public class SessionController {
entity.setRollen(mitspieler.getRollen());
entity.setSpieltMit(mitspieler.getSpieltMit());
entity.setWerkzeuge(mitspieler.getVerfuegbareWerkzeuge());
entity.setUserId(mitspieler.getUserId());
entity.setEigenesGeraet(mitspieler.isEigenesGeraet());
entity.setSession(session);
mitspielerRepository.save(entity);
return ResponseEntity.accepted().build();
@@ -184,9 +221,9 @@ public class SessionController {
@GetMapping("/{sessionId}/finisher")
public ResponseEntity<List<AufgabeAnzeige>> getFinisher(@PathVariable UUID sessionId) {
try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.badRequest().build();
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session);
BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
return ResponseEntity.ok(durchfuehren.getFinisher());
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
@@ -197,9 +234,9 @@ public class SessionController {
@PostMapping("/{sessionId}/backToLevel5")
public ResponseEntity<Void> backToLevel5(@PathVariable UUID sessionId) {
try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.badRequest().build();
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session);
BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
durchfuehren.backToLvl5();
session.setLevel(durchfuehren.getLevel());
session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel());
@@ -211,8 +248,62 @@ public class SessionController {
}
}
private Session toSession(SessionEntity entity) {
Session session = new Session();
@GetMapping("/{sessionId}/mitspieler/me")
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.setUserId(entity.getUserId());
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.session.Mitspieler;
import de.oaa.xxx.session.entity.AktiveSperreEntity;
import de.oaa.xxx.session.entity.SessionEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository;
import de.oaa.xxx.session.repository.MitspielerRepository;
import de.oaa.xxx.session.repository.SessionRepository;
import de.oaa.xxx.session.sperre.SperreCallback;
import de.oaa.xxx.session.sperre.SperreVerarbeiten;
import de.oaa.xxx.session.sperre.SperrenVerlaengernCallback;
import de.oaa.xxx.games.bdsm.AktiveSperre;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import de.oaa.xxx.games.bdsm.sperre.SperreCallback;
import de.oaa.xxx.games.bdsm.sperre.SperreVerarbeiten;
import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
@@ -26,18 +26,18 @@ import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@RestController("sessionSperreController")
@RequestMapping("/session/sperre")
@RestController("bdsmSperreController")
@RequestMapping("/bdsm/sperre")
@Transactional
public class SperreController {
private static final Logger LOGGER = LoggerFactory.getLogger(SperreController.class);
private final SessionRepository sessionRepository;
private final BdsmGameRepository sessionRepository;
private final MitspielerRepository mitspielerRepository;
private final AktiveSperreRepository aktiveSperreRepository;
public SperreController(SessionRepository sessionRepository, MitspielerRepository mitspielerRepository,
public SperreController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository,
AktiveSperreRepository aktiveSperreRepository) {
this.sessionRepository = sessionRepository;
this.mitspielerRepository = mitspielerRepository;
@@ -78,7 +78,7 @@ public class SperreController {
@GetMapping("/aktive")
public ResponseEntity<List<AktiveSperre>> getAktiveSperren(@RequestParam UUID sessionId) {
try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.noContent().build();
List<Mitspieler> mitspielerList = session.getMitspieler().stream()
.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.session.Mitspieler;
import de.oaa.xxx.session.Werkzeug;
import de.oaa.xxx.games.bdsm.AktiveSperre;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.Werkzeug;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
@@ -50,7 +50,7 @@ public class AktiveSperreEntity {
private String releaseText;
@ManyToOne
@JoinColumn(name = "sessionId", nullable = false)
private SessionEntity session;
private BdsmGameEntity session;
public AktiveSperre toSperre(List<Mitspieler> mitspielerList) {
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.Entity;
@@ -18,7 +18,7 @@ import java.util.UUID;
@Setter
@Entity
@Table(name = "session")
public class SessionEntity {
public class BdsmGameEntity {
@Id
@Column
@@ -47,10 +47,16 @@ public class SessionEntity {
private String aufgaben;
@Column
private Double zeitfaktorZeitstrafen;
@Column(columnDefinition = "TEXT")
private String activeTaskJson;
@Column
private LocalDateTime taskStartedAt;
@Column
private UUID setupId;
@Override
public String toString() {
return "SessionEntity[sessionId=" + sessionId + ", userId=" + userId
return "BdsmGameEntity[sessionId=" + sessionId + ", userId=" + userId
+ ", level=" + level + ", aufgaben=" + aufgabenAufAktuellemLevel + "/" + aufgabenProLevel
+ ", pStrafe=" + wahrscheinlichkeitStrafe + "%, pSperre=" + wahrscheinlichkeitSperre + "%"
+ ", zeitfaktor=" + zeitfaktorZeitstrafen + ", start=" + startZeit + "]";

View File

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

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 de.oaa.xxx.session.aufgaben.AufgabenList;
import de.oaa.xxx.session.aufgaben.Sperre;
import de.oaa.xxx.session.entity.AktiveSperreEntity;
import de.oaa.xxx.session.entity.MitspielerEntity;
import de.oaa.xxx.session.entity.SessionEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository;
import de.oaa.xxx.session.repository.MitspielerRepository;
import de.oaa.xxx.session.repository.SessionRepository;
import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
import de.oaa.xxx.games.bdsm.aufgaben.Sperre;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import java.time.LocalDateTime;
import java.util.Optional;
@@ -19,9 +19,9 @@ public class SperreVerarbeiten {
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 {
SessionEntity session = sessionRepository.findById(callback.getSessionId()).orElse(null);
BdsmGameEntity session = sessionRepository.findById(callback.getSessionId()).orElse(null);
MitspielerEntity mitspieler = mitspielerRepository.findById(callback.getSpielerId()).orElse(null);
if (session != null) {
AufgabenList aufgaben = objectMapper.readValue(session.getAufgaben(), AufgabenList.class);
@@ -56,7 +56,7 @@ public class SperreVerarbeiten {
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) {
aktiv.setAktiveSperreId(UUID.randomUUID());
LocalDateTime now = LocalDateTime.now();
@@ -70,7 +70,7 @@ public class SperreVerarbeiten {
aktiv.setReleaseText(callback.getReleaseText());
}
private Integer berechneDauer(SessionEntity session, Sperre sperre) {
private Integer berechneDauer(BdsmGameEntity session, Sperre sperre) {
Integer minuten = 30;
if (sperre.getMinutenVon() != 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;

View File

@@ -22,9 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
import de.oaa.xxx.social.SseService;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository;
@RestController
@@ -34,8 +32,7 @@ public class LockeeInvitationController {
private final LockeeInvitationRepository lockeeInvitationRepository;
private final CardlockRepository cardlockRepository;
private final UserRepository userRepository;
private final MessageRepository messageRepository;
private final SseService sseService;
private final SystemMessageService systemMessageService;
@Value("${app.base-url:http://localhost:8080}")
private String baseUrl;
@@ -45,27 +42,15 @@ public class LockeeInvitationController {
public LockeeInvitationController(LockeeInvitationRepository lockeeInvitationRepository,
CardlockRepository cardlockRepository,
UserRepository userRepository,
MessageRepository messageRepository,
SseService sseService) {
SystemMessageService systemMessageService) {
this.lockeeInvitationRepository = lockeeInvitationRepository;
this.cardlockRepository = cardlockRepository;
this.userRepository = userRepository;
this.messageRepository = messageRepository;
this.sseService = sseService;
this.systemMessageService = systemMessageService;
}
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl) {
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));
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl, de.oaa.xxx.social.entity.MessageCause cause) {
systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
}
private String generateUnlockCode(int lines) {
@@ -154,7 +139,7 @@ public class LockeeInvitationController {
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getLockeeUserId(),
me.getName() + " hat die Lockee-Einladung für das Lock „" + lockName + "\" zurückgezogen.",
null);
null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
}
return ResponseEntity.noContent().build();
@@ -248,7 +233,7 @@ public class LockeeInvitationController {
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getKeyholderUserId(),
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" angenommen.",
"/keyholder.html");
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
return ResponseEntity.ok(Map.of(
"lockId", lock.getLockId().toString(),
@@ -278,7 +263,7 @@ public class LockeeInvitationController {
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getKeyholderUserId(),
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" abgelehnt.",
null);
null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
}
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.LockeeInvitationEntity;
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.AssignedTaskRepository;
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.VerificationVoteEntity;
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
import de.oaa.xxx.social.SseService;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository;
@RestController
@@ -65,15 +63,14 @@ public class CardLockController {
private final VerificationRepository verificationRepository;
private final VerificationVoteRepository verificationVoteRepository;
private final HygieneViolationRepository hygieneViolationRepository;
private final MessageRepository messageRepository;
private final LockeeInvitationRepository lockeeInvitationRepository;
private final AssignedTaskRepository assignedTaskRepository;
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
private final CommunityTaskVoteRepository communityTaskVoteRepository;
private final UnlockCodeHistoryRepository unlockCodeHistoryRepository;
private final UnlockCodeHistoryService unlockCodeHistoryService;
private final LockHistoryRepository lockHistoryRepository;
private final SseService sseService;
private final GameHistoryRepository gameHistoryRepository;
private final SystemMessageService systemMessageService;
@Value("${app.base-url:http://localhost:8080}")
private String baseUrl;
@@ -85,15 +82,14 @@ public class CardLockController {
VerificationRepository verificationRepository,
VerificationVoteRepository verificationVoteRepository,
HygieneViolationRepository hygieneViolationRepository,
MessageRepository messageRepository,
LockeeInvitationRepository lockeeInvitationRepository,
AssignedTaskRepository assignedTaskRepository,
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
CommunityTaskVoteRepository communityTaskVoteRepository,
UnlockCodeHistoryRepository unlockCodeHistoryRepository,
UnlockCodeHistoryService unlockCodeHistoryService,
LockHistoryRepository lockHistoryRepository,
SseService sseService) {
GameHistoryRepository gameHistoryRepository,
SystemMessageService systemMessageService) {
this.cardlockRepository = cardlockRepository;
this.cardLockRepository = cardLockRepository;
this.userRepository = userRepository;
@@ -101,15 +97,14 @@ public class CardLockController {
this.verificationRepository = verificationRepository;
this.verificationVoteRepository = verificationVoteRepository;
this.hygieneViolationRepository = hygieneViolationRepository;
this.messageRepository = messageRepository;
this.lockeeInvitationRepository = lockeeInvitationRepository;
this.assignedTaskRepository = assignedTaskRepository;
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
this.communityTaskVoteRepository = communityTaskVoteRepository;
this.unlockCodeHistoryRepository = unlockCodeHistoryRepository;
this.unlockCodeHistoryService = unlockCodeHistoryService;
this.lockHistoryRepository = lockHistoryRepository;
this.sseService = sseService;
this.gameHistoryRepository = gameHistoryRepository;
this.systemMessageService = systemMessageService;
}
record CreateCardLockRequest(
@@ -194,7 +189,7 @@ public class CardLockController {
String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock";
sendMessage(myId, lockee.getUserId(),
me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.",
"/einladungen.html");
"/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
return ResponseEntity.ok(Map.of(
"lockId", lock.getLockId().toString(),
@@ -257,7 +252,7 @@ public class CardLockController {
String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock";
sendMessage(me.getUserId(), kh.getUserId(),
me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.",
"/einladungen.html");
"/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
keyholderPending = true;
}
@@ -282,7 +277,7 @@ public class CardLockController {
var l = lockOpt.get();
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();
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 ->
sendMessage(l.getLockee(), kh.getUserId(),
"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";
} else if ("COMMUNITY".equals(l.getTaskCardMode())) {
@@ -320,9 +315,14 @@ public class CardLockController {
result.put("unlockCode", dto.unlockCode() != null ? dto.unlockCode() : "");
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()) {
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);
@@ -417,7 +417,7 @@ public class CardLockController {
var l = lockOpt.get();
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();
return ResponseEntity.noContent().build();
}
@@ -434,8 +434,16 @@ public class CardLockController {
var l = lockOpt.get();
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();
// 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();
}
@@ -557,7 +565,7 @@ public class CardLockController {
lockDirty = true;
sendMessage(l.getKeyholder(), l.getLockee(),
"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);
@@ -695,7 +703,7 @@ public class CardLockController {
var lockee = meOpt.get();
sendMessage(myId, lock.getKeyholder(),
"📸 " + lockee.getName() + " hat eine Verifikation eingereicht.",
"/keyholder.html");
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
}
return ResponseEntity.noContent().build();
@@ -793,14 +801,9 @@ public class CardLockController {
if (lockOpt.isPresent()) {
var lock = lockOpt.get();
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(myId);
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);
sendMessage(myId, lock.getLockee(),
me.getName() + " hat die Einladung als Keyholder*In für das Lock „" + lockName + "\" abgelehnt.",
null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
}
return ResponseEntity.noContent().build();
@@ -855,14 +858,9 @@ public class CardLockController {
String lockName = lockOpt.get().getName() != null && !lockOpt.get().getName().isBlank()
? lockOpt.get().getName() : "Unbenanntes Lock";
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(myId);
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);
sendMessage(myId, inv.getKeyholderUserId(),
me.getName() + " hat die Keyholder-Einladung für das Lock „" + lockName + "\" zurückgezogen.",
null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
return ResponseEntity.noContent().build();
}
@@ -1069,7 +1067,7 @@ public class CardLockController {
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
// 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());
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 Karten zu deinem Lock hinzugefügt.";
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(myId);
msg.setReceiverId(l.getLockee());
msg.setText(msgText);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg);
sendMessage(myId, l.getLockee(), msgText, "/activelock.html?lockId=" + lockId,
de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
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 Karten aus deinem Lock entfernt.";
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(myId);
msg.setReceiverId(l.getLockee());
msg.setText(msgText);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg);
sendMessage(myId, l.getLockee(), msgText, "/activelock.html?lockId=" + lockId,
de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build();
}
@@ -1221,19 +1207,8 @@ public class CardLockController {
}
}
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl) {
if (senderId == null || receiverId == null) return;
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));
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl, de.oaa.xxx.social.entity.MessageCause cause) {
systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
}
@GetMapping("/cardlock/unlock-history")
@@ -1305,7 +1280,7 @@ public class CardLockController {
sendMessage(me.getUserId(), l.getLockee(),
me.getName() + " hat dir eine Aufgabe gestellt. Du hast " +
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();
}
@@ -1365,7 +1340,7 @@ public class CardLockController {
assignedTaskRepository.save(task);
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();
}
@@ -1397,7 +1372,7 @@ public class CardLockController {
assignedTaskRepository.save(task);
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();
}
@@ -1465,7 +1440,7 @@ public class CardLockController {
until.toLocalDate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")) +
" " + until.toLocalTime().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm")) +
" Uhr eingefroren.",
"/activelock.html?lockId=" + lockId);
"/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build();
}
@@ -1490,7 +1465,7 @@ public class CardLockController {
cardlockRepository.save(l);
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();
}
@@ -1512,7 +1487,7 @@ public class CardLockController {
sendMessage(myId, l.getLockee(),
"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();
}
@@ -1542,7 +1517,7 @@ public class CardLockController {
// Keyholderin benachrichtigen
sendMessage(myId, l.getKeyholder(),
"⚠️ 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);
return ResponseEntity.noContent().build();

View File

@@ -11,10 +11,9 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.oaa.xxx.games.chastity.LockType;
import de.oaa.xxx.games.chastity.ProcessLock;
import de.oaa.xxx.games.chastity.history.LockHistoryEntity;
import de.oaa.xxx.games.chastity.history.LockHistoryRepository;
import de.oaa.xxx.games.history.GameHistoryEntity;
import de.oaa.xxx.games.history.GameHistoryRepository;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.verification.VerificationEntity;
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
@@ -29,15 +28,15 @@ public class CardLockService extends ProcessLock {
private VerificationRepository verificationRepository;
private VerificationVoteRepository verificationVoteRepository;
private CardLockRepository cardLockRepository;
private LockHistoryRepository lockHistoryRepository;
private GameHistoryRepository gameHistoryRepository;
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.verificationRepository = verificationRepository;
this.verificationVoteRepository = verificationVoteRepository;
this.cardLockRepository = cardLockRepository;
this.lockHistoryRepository = lockHistoryRepository;
this.gameHistoryRepository = gameHistoryRepository;
this.userRepository = userRepository;
}
@@ -101,28 +100,30 @@ public class CardLockService extends ProcessLock {
public void unlock(String unlockCode) {
this.lock.setUnlockTime(LocalDateTime.now());
// Self-Lock oder automatische Entsperrung ohne Keyholder-Zustimmung → ungültig
boolean valid = lock.getKeyholder() != null && !lock.isEmergencyAutoUnlocked();
if (!this.lock.isTestLock()) {
if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) {
Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream()
.filter(verification -> isValid(verification))
.map(verification -> verification.getVerificationTime().toLocalDate())
.collect(Collectors.toSet());
LocalDate current = this.lock.getStartTime().toLocalDate();
LocalDate last = this.lock.getUnlockTime().toLocalDate().minusDays(1);
while (!current.isAfter(last)) {
if (!verifications.contains(current)) {
valid = false;
break;
}
current = current.plusDays(1);
boolean valid = true;
if (lock.isEmergencyAutoUnlocked()) {
valid = false;
LOGGER.debug("Lock invalid - Emergency Auto-Unlock (1h timer)");
}
if (lock.isTestLock()) {
valid = false;
} else if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) {
Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream()
.filter(verification -> isValid(verification))
.map(verification -> verification.getVerificationTime().toLocalDate())
.collect(Collectors.toSet());
LocalDate current = this.lock.getStartTime().toLocalDate();
LocalDate last = this.lock.getUnlockTime().toLocalDate().minusDays(1);
while (!current.isAfter(last)) {
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());
@@ -132,31 +133,18 @@ public class CardLockService extends ProcessLock {
if (valid) {
long durationMinutes = Duration.between(lock.getStartTime(), lock.getUnlockTime()).toMinutes();
// Eintrag für den Lockee
LockHistoryEntity lockeeEntry = new LockHistoryEntity();
lockeeEntry.setUserId(lock.getLockee());
lockeeEntry.setLockedBy(lock.getKeyholder());
lockeeEntry.setLockName(lock.getName());
lockeeEntry.setStartTime(lock.getStartTime());
lockeeEntry.setEndTime(lock.getUnlockTime());
lockeeEntry.setType(LockType.CARD);
lockeeEntry.setDurationMinutes(durationMinutes);
lockeeEntry.setRole("LOCKEE");
lockHistoryRepository.save(lockeeEntry);
// Eintrag für die Keyholderin
// Gemeinsamer History-Eintrag mit Teilnehmerliste
GameHistoryEntity entry = new GameHistoryEntity();
entry.setGameType(de.oaa.xxx.games.history.GameType.CARDLOCK);
entry.setGameName(lock.getName());
entry.setStartTime(lock.getStartTime());
entry.setEndTime(lock.getUnlockTime());
entry.setDurationMinutes(durationMinutes);
entry.addParticipant(lock.getLockee(), de.oaa.xxx.games.history.GameRole.LOCKEE);
if (lock.getKeyholder() != null) {
LockHistoryEntity khEntry = new LockHistoryEntity();
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);
entry.addParticipant(lock.getKeyholder(), de.oaa.xxx.games.history.GameRole.KEYHOLDER);
}
gameHistoryRepository.save(entry);
int minutes = (int) durationMinutes;
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.AssignedTaskRepository;
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.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
@@ -25,8 +23,7 @@ public class TaskCardController {
private final CommunityTaskVoteRepository communityTaskVoteRepository;
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
private final AssignedTaskRepository assignedTaskRepository;
private final MessageRepository messageRepository;
private final SseService sseService;
private final SystemMessageService systemMessageService;
public TaskCardController(CardlockRepository cardlockRepository,
UserRepository userRepository,
@@ -34,16 +31,14 @@ public class TaskCardController {
CommunityTaskVoteRepository communityTaskVoteRepository,
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
AssignedTaskRepository assignedTaskRepository,
MessageRepository messageRepository,
SseService sseService) {
SystemMessageService systemMessageService) {
this.cardlockRepository = cardlockRepository;
this.userRepository = userRepository;
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
this.communityTaskVoteRepository = communityTaskVoteRepository;
this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository;
this.assignedTaskRepository = assignedTaskRepository;
this.messageRepository = messageRepository;
this.sseService = sseService;
this.systemMessageService = systemMessageService;
}
// ── Keyholder: ausstehende Aufgaben-Karten-Entscheidungen ─────────────────
@@ -240,16 +235,6 @@ public class TaskCardController {
}
private void sendMessage(UUID fromId, UUID toId, String text, String targetUrl) {
if (toId == null) return;
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));
systemMessageService.send(fromId, toId, text, targetUrl, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
}
}

View File

@@ -4,7 +4,6 @@ import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;
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.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.social.SseService;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.social.SystemMessageService;
@Component
public class TaskVoteScheduler {
@@ -30,21 +27,18 @@ public class TaskVoteScheduler {
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
private final CardlockRepository cardlockRepository;
private final AssignedTaskRepository assignedTaskRepository;
private final MessageRepository messageRepository;
private final SseService sseService;
private final SystemMessageService systemMessageService;
public TaskVoteScheduler(CommunityTaskVoteRepository communityTaskVoteRepository,
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
CardlockRepository cardlockRepository,
AssignedTaskRepository assignedTaskRepository,
MessageRepository messageRepository,
SseService sseService) {
SystemMessageService systemMessageService) {
this.communityTaskVoteRepository = communityTaskVoteRepository;
this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository;
this.cardlockRepository = cardlockRepository;
this.assignedTaskRepository = assignedTaskRepository;
this.messageRepository = messageRepository;
this.sseService = sseService;
this.systemMessageService = systemMessageService;
}
@Scheduled(fixedDelay = 60_000)
@@ -117,16 +111,6 @@ public class TaskVoteScheduler {
}
private void sendMessage(UUID toId, String text, String targetUrl) {
if (toId == null) return;
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));
systemMessageService.send(toId, toId, text, targetUrl, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
}
}

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

View File

@@ -3,6 +3,7 @@ package de.oaa.xxx.registration;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDate;
import java.util.UUID;
@Getter
@@ -13,6 +14,7 @@ public class Registration {
private String name;
private String email;
private String passwordHash;
private LocalDate geburtsdatum;
@Override
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.user.UserRepository;
import java.time.LocalDate;
import java.time.Period;
@RestController
@RequestMapping("/registration")
public class RegistrationController {
@@ -39,6 +42,11 @@ public class RegistrationController {
@PostMapping
public ResponseEntity<String> create(@RequestBody Registration 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()
|| userRepository.findByEmail(registration.getEmail()).isPresent()) {
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.Setter;
import java.time.LocalDate;
import java.util.UUID;
@Getter
@@ -26,6 +27,8 @@ public class RegistrationEntity {
private String password;
@Column
private Boolean activated;
@Column
private LocalDate geburtsdatum;
@Override
public String toString() {
@@ -38,6 +41,7 @@ public class RegistrationEntity {
registration.setEmail(email);
registration.setName(name);
registration.setPasswordHash(password);
registration.setGeburtsdatum(geburtsdatum);
return registration;
}
@@ -48,6 +52,7 @@ public class RegistrationEntity {
entity.setActivated(Boolean.FALSE);
entity.setName(registration.getName());
entity.setPassword(registration.getPasswordHash());
entity.setGeburtsdatum(registration.getGeburtsdatum());
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.entity.FriendshipEntity;
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.repository.FriendshipRepository;
import de.oaa.xxx.social.repository.MessageRepository;
@@ -31,15 +32,18 @@ public class SocialController {
private final FriendshipRepository friendshipRepository;
private final MessageRepository messageRepository;
private final SseService sseService;
private final SystemMessageService systemMessageService;
public SocialController(UserRepository userRepository,
FriendshipRepository friendshipRepository,
MessageRepository messageRepository,
SseService sseService) {
SseService sseService,
SystemMessageService systemMessageService) {
this.userRepository = userRepository;
this.friendshipRepository = friendshipRepository;
this.messageRepository = messageRepository;
this.sseService = sseService;
this.systemMessageService = systemMessageService;
}
record FriendRequestBody(UUID receiverId) {}
@@ -94,6 +98,13 @@ public class SocialController {
f.setCreatedAt(LocalDateTime.now());
friendshipRepository.save(f);
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();
}
@@ -299,22 +310,52 @@ public class SocialController {
// ── Helpers ──
private UserProfile toUserProfileWithStatus(UserEntity user, UUID myId) {
boolean isOwn = user.getUserId().equals(myId);
String status = "NONE";
var existing = friendshipRepository.findExisting(myId, user.getUserId());
if (existing.isPresent()) {
FriendshipEntity f = existing.get();
if (f.getStatus() == Status.ACCEPTED) {
status = "FRIEND";
} else if (f.getSenderId().equals(myId)) {
status = "PENDING_SENT";
} else {
status = "PENDING_RECEIVED";
if (!isOwn) {
var existing = friendshipRepository.findExisting(myId, user.getUserId());
if (existing.isPresent()) {
FriendshipEntity f = existing.get();
if (f.getStatus() == Status.ACCEPTED) {
status = "FRIEND";
} else if (f.getSenderId().equals(myId)) {
status = "PENDING_SENT";
} else {
status = "PENDING_RECEIVED";
}
}
}
return new UserProfile(user.getUserId(), user.getName(), user.getProfilePicture(), user.getProfilePictureHq(),
status, user.getAlter(), user.getGroesse(), user.getGewicht(),
user.getGeschlecht(), user.getNeigung(), user.getBeziehungsstatus(), user.getBeschreibung(),
user.getLockeeXp(), user.getKeyholderXp());
boolean isFriend = isOwn || "FRIEND".equals(status);
// Grunddaten nur zurückgeben wenn berechtigt
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) {

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.Geschlecht;
import de.oaa.xxx.user.Neigung;
import de.oaa.xxx.user.Sichtbarkeit;
import java.util.UUID;
@@ -20,10 +21,20 @@ public record UserProfile(
Beziehungsstatus beziehungsstatus,
String beschreibung,
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.) */
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)
private boolean systemMessage = false;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private MessageCause messageCause;
@Column(length = 500)
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.Setter;
import java.time.LocalDate;
import java.time.Period;
import java.util.UUID;
@Getter
@@ -14,7 +16,7 @@ public class User {
private String email;
private String password;
private String profilePicture;
private Integer alter;
private LocalDate geburtsdatum;
private Integer groesse;
private Integer gewicht;
private Geschlecht geschlecht;
@@ -22,6 +24,10 @@ public class User {
private Beziehungsstatus beziehungsstatus;
private String beschreibung;
public Integer getAlter() {
return geburtsdatum != null ? Period.between(geburtsdatum, LocalDate.now()).getYears() : null;
}
@Override
public String toString() {
return "User[userId=" + userId + ", name=" + name + ", email=" + email + "]";

View File

@@ -1,8 +1,13 @@
package de.oaa.xxx.user;
import java.security.Principal;
import java.time.LocalDate;
import java.time.Period;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -10,6 +15,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
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.PutMapping;
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 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.FavoritRepository;
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.ToyRepository;
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.registration.Registration;
import de.oaa.xxx.registration.RegistrationRepository;
import de.oaa.xxx.session.entity.AktiveSperreEntity;
import de.oaa.xxx.session.entity.MitspielerEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository;
import de.oaa.xxx.session.repository.MitspielerRepository;
import de.oaa.xxx.session.repository.SessionRepository;
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
import de.oaa.xxx.social.repository.ProfileImageRepository;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
import de.oaa.xxx.social.repository.KommentarLikeRepository;
import de.oaa.xxx.social.repository.KommentarRepository;
import de.oaa.xxx.social.repository.NotificationPreferenceRepository;
import de.oaa.xxx.social.repository.PinnwandEintragRepository;
import de.oaa.xxx.social.repository.PinnwandLikeRepository;
import de.oaa.xxx.social.repository.KommentarRepository;
import de.oaa.xxx.social.repository.KommentarLikeRepository;
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
import de.oaa.xxx.social.repository.ProfileImageRepository;
import jakarta.transaction.Transactional;
@RestController
@@ -54,7 +66,7 @@ public class UserController {
private final ToyRepository toyRepository;
private final FavoritRepository favoritRepository;
private final GruppenAboRepository gruppenAboRepository;
private final SessionRepository sessionRepository;
private final BdsmGameRepository sessionRepository;
private final AktiveSperreRepository aktiveSperreRepository;
private final MitspielerRepository mitspielerRepository;
private final EmailChangeRepository emailChangeRepository;
@@ -65,6 +77,8 @@ public class UserController {
private final PinnwandLikeRepository pinnwandLikeRepository;
private final KommentarRepository kommentarRepository;
private final KommentarLikeRepository kommentarLikeRepository;
private final NotificationPreferenceRepository notificationPreferenceRepository;
private final BdsmDefaultsRepository bdsmDefaultsRepository;
public UserController(UserRepository userRepository,
RegistrationRepository registrationRepository,
@@ -75,7 +89,7 @@ public class UserController {
ToyRepository toyRepository,
FavoritRepository favoritRepository,
GruppenAboRepository gruppenAboRepository,
SessionRepository sessionRepository,
BdsmGameRepository sessionRepository,
AktiveSperreRepository aktiveSperreRepository,
MitspielerRepository mitspielerRepository,
EmailChangeRepository emailChangeRepository,
@@ -85,7 +99,9 @@ public class UserController {
PinnwandEintragRepository pinnwandEintragRepository,
PinnwandLikeRepository pinnwandLikeRepository,
KommentarRepository kommentarRepository,
KommentarLikeRepository kommentarLikeRepository) {
KommentarLikeRepository kommentarLikeRepository,
NotificationPreferenceRepository notificationPreferenceRepository,
BdsmDefaultsRepository bdsmDefaultsRepository) {
this.userRepository = userRepository;
this.registrationRepository = registrationRepository;
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
@@ -106,12 +122,23 @@ public class UserController {
this.pinnwandLikeRepository = pinnwandLikeRepository;
this.kommentarRepository = kommentarRepository;
this.kommentarLikeRepository = kommentarLikeRepository;
this.notificationPreferenceRepository = notificationPreferenceRepository;
this.bdsmDefaultsRepository = bdsmDefaultsRepository;
}
record ProfilePictureRequest(String picture, String pictureHq) {}
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) {}
record PrivacyRequest(
Sichtbarkeit sichtbarkeitGrunddaten,
Sichtbarkeit sichtbarkeitGalerie,
Sichtbarkeit sichtbarkeitFreunde,
Sichtbarkeit sichtbarkeitFeed,
Sichtbarkeit sichtbarkeitPinnwand,
Sichtbarkeit sichtbarkeitXp,
Sichtbarkeit sichtbarkeitLockhistorie) {}
@PutMapping("/me/picture")
public ResponseEntity<Void> updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) {
@@ -132,7 +159,6 @@ public class UserController {
if (request.beschreibung() != null && request.beschreibung().length() > 600) {
return ResponseEntity.badRequest().build();
}
user.setAlter(request.alter());
user.setGroesse(request.groesse());
user.setGewicht(request.gewicht());
user.setGeschlecht(request.geschlecht());
@@ -144,6 +170,126 @@ public class UserController {
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")
public ResponseEntity<Void> updateName(@RequestBody NameChangeRequest request, Principal principal) {
String newName = request.name();
@@ -264,7 +410,14 @@ public class UserController {
entity.setEmail(registration.getEmail());
entity.setName(registration.getName());
entity.setPassword(registration.getPasswordHash());
entity.setGeburtsdatum(registration.getGeburtsdatum());
userRepository.save(entity);
for (MessageCause cause : MessageCause.values()) {
notificationPreferenceRepository.save(
NotificationPreferenceEntity.defaultFor(entity.getUserId(), cause));
}
return ResponseEntity.status(201).build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);

View File

@@ -4,6 +4,8 @@ import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDate;
import java.time.Period;
import java.util.UUID;
@Getter
@@ -28,8 +30,8 @@ public class UserEntity {
@Column(columnDefinition = "MEDIUMTEXT")
private String profilePictureHq;
@Column(name = "benutzer_alter")
private Integer alter;
@Column
private LocalDate geburtsdatum;
@Column
private Integer groesse;
@@ -58,6 +60,39 @@ public class UserEntity {
@Column(nullable = false, columnDefinition = "INT DEFAULT 0")
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
public String toString() {
return "UserEntity[userId=" + userId + ", name=" + name + ", email=" + email + "]";
@@ -69,7 +104,7 @@ public class UserEntity {
user.setName(name);
user.setUserId(userId);
user.setProfilePicture(profilePicture);
user.setAlter(alter);
user.setGeburtsdatum(geburtsdatum);
user.setGroesse(groesse);
user.setGewicht(gewicht);
user.setGeschlecht(geschlecht);

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<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>Aufgaben XXX The Game</title>
@@ -608,44 +608,47 @@
resetSelection();
document.getElementById('userLoading').style.display = 'block';
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 => {
console.log('[aufgaben] user gruppen:', data);
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);
document.getElementById('userLoading').style.display = 'none';
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() {
resetSelection();
document.getElementById('systemLoading').style.display = 'block';
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 => {
console.log('[aufgaben] system gruppen:', data);
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);
document.getElementById('systemLoading').style.display = 'none';
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() {
document.getElementById('aboLoading').style.display = 'block';
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 => {
console.log('[aufgaben] abo gruppen:', data);
aboTotalPages = data.totalPages || 1;
renderGruppen('aboList', data.content, 'abo');
updatePaging('aboPaging', 'aboPrev', 'aboNext', 'aboPageInfo', aboPage, aboTotalPages);
document.getElementById('aboLoading').style.display = 'none';
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() {

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>
<html lang="de">
<head>
<meta charset="UTF-8">
<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 Neue Session XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { max-width: 540px; }
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
@@ -180,7 +180,7 @@
aufgabenProLevel: parseInt(document.getElementById('sldAufgaben').value),
zeitfaktorZeitstrafen: parseInt(document.getElementById('sldZeit').value) / 10,
}));
window.location.href = '/sessionbdsmplayers.html';
window.location.href = '/bdsmplayers.html';
}
function showMessage(text, type) {
@@ -224,7 +224,7 @@
function sessionFortfahren(sid) {
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
sessionStorage.setItem('bdsm-session-id', sid);
window.location.href = '/sessionbdsmingame.html';
window.location.href = '/bdsmingame.html';
}
function sessionBeendenFragen(sid) {
@@ -241,7 +241,7 @@
async function sessionLoeschen(sid) {
versteckeModal();
try {
await fetch('/session', {
await fetch('/bdsm', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: sid }),
@@ -256,7 +256,7 @@
if (!meRes.ok) return;
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.ok) return;

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<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 Im Spiel XXX The Game</title>
@@ -236,7 +236,12 @@
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup') || 'null');
const toys = JSON.parse(sessionStorage.getItem('bdsm-session-toys') || '[]');
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 ──
function zeigeModal(title, text, actions) {
@@ -283,6 +288,7 @@
function clearTimer() {
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
stopHostPoll();
}
function zeigeTaskFehler(text) {
@@ -310,7 +316,7 @@
card.innerHTML = 'Aufgabe wird geladen…';
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.ok) throw new Error(`HTTP ${res.status}`);
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() {
const task = currentTask;
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);
else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(task);
else if (task.timer != null) zeigeTimerAufgabe(task);
@@ -380,6 +566,7 @@
actions.innerHTML = `
<div class="timer-big" id="timerValue">${formatTime(remaining)}</div>
<button class="btn-sm-cancel" onclick="timerAbbrechen()">✕ Abbrechen</button>`;
saveAktiveAufgabe(task, new Date().toISOString());
timerInterval = setInterval(() => {
remaining--;
@@ -405,8 +592,10 @@
async function aufgabeAbgeschlossen() {
clearTimer();
await clearAktiveAufgabe();
if (isGuest) { gastAufgabeAbgeschlossen(); return; }
try {
const res = await fetch(`/session/sperre/abgelaufene?sessionId=${sessionId}`);
const res = await fetch(`/bdsm/sperre/abgelaufene?sessionId=${sessionId}`);
if (res.ok) {
const text = await res.text();
const texte = text.split(';').map(t => t.trim()).filter(t => t.length > 0);
@@ -429,7 +618,7 @@
const cb = currentTask?.callback;
if (!cb) return;
try {
const res = await fetch('/session/sperre', {
const res = await fetch('/bdsm/sperre', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...cb, sessionId }),
@@ -445,7 +634,7 @@
const cb = currentTask?.callback;
if (!cb) return;
try {
const res = await fetch('/session/sperre/verlaengern', {
const res = await fetch('/bdsm/sperre/verlaengern', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...cb, sessionId }),
@@ -478,14 +667,14 @@
async function zurueckZuLevel5() {
try {
await fetch(`/session/${sessionId}/backToLevel5`, { method: 'POST' });
await fetch(`/bdsm/${sessionId}/backToLevel5`, { method: 'POST' });
} catch (_) {}
ladeAufgabe();
}
async function starteFinale() {
try {
const res = await fetch(`/session/sperre/aktive?sessionId=${sessionId}`);
const res = await fetch(`/bdsm/sperre/aktive?sessionId=${sessionId}`);
if (res.ok) {
const sperren = await res.json();
const texte = (sperren || []).map(s => s.releaseText).filter(t => t);
@@ -506,7 +695,7 @@
async function ladeFinisher() {
try {
const res = await fetch(`/session/${sessionId}/finisher`);
const res = await fetch(`/bdsm/${sessionId}/finisher`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const liste = await res.json();
naechsterFinisher(liste, 0);
@@ -572,7 +761,7 @@
async function sessionLoeschen() {
versteckeModal();
try {
await fetch('/session', {
await fetch('/bdsm', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId }),
@@ -583,7 +772,11 @@
}
// ── Start ──
ladeAufgabe();
if (isGuest) {
startGastPoll();
} else {
checkAktiveAufgabe();
}
</script>
</body>
</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>
<html lang="de">
<head>
<meta charset="UTF-8">
<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 Aufgaben-Gruppen XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { max-width: 700px; }
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
@@ -119,7 +119,7 @@
<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 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>
</div>
</div>
@@ -130,7 +130,7 @@
<script src="/js/sidebar.js"></script>
<script>
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') || '[]'));
@@ -308,18 +308,19 @@
}
sessionStorage.setItem('bdsm-session-gruppen', JSON.stringify(selected));
window.location.href = '/sessionbdsmtoys.html';
window.location.href = '/bdsmtoys.html';
}
Promise.all([
fetch('/gruppe/list/user?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
fetch('/abo/list?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() : { 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() : (console.warn('[bdsmtasks] abo HTTP', r.status), { 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]) => {
console.log('[bdsmtasks] own:', own, 'abo:', abo, 'system:', system);
renderList('listOwn', own.content || []);
renderList('listSubscribed', abo.content || []);
renderList('listSystem', system.content || []);
});
}).catch(err => console.error('[bdsmtasks] Fehler beim Laden:', err));
</script>
</body>
</html>

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<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 Toys XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { max-width: 700px; }
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
@@ -74,7 +74,7 @@
<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 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>
</div>
</div>
@@ -85,7 +85,7 @@
<script src="/js/sidebar.js"></script>
<script>
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)
const savedToysRaw = sessionStorage.getItem('bdsm-session-toys');
@@ -241,7 +241,7 @@
try {
// 1. Session anlegen
const sessionRes = await fetch('/session', {
const sessionRes = await fetch('/bdsm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -249,6 +249,7 @@
wahrscheinlichkeitSperre: settings.wahrscheinlichkeitSperre,
aufgabenProLevel: settings.aufgabenProLevel,
zeitfaktorZeitstrafen: settings.zeitfaktorZeitstrafen,
setupId: sessionStorage.getItem('bdsm-setup-id'),
}),
});
if (!sessionRes.ok) throw new Error('Session konnte nicht angelegt werden.');
@@ -258,7 +259,7 @@
// 2. Mitspieler hinzufügen
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
for (const p of setup.mitspieler) {
const res = await fetch(`/session/${sessionId}/mitspieler`, {
const res = await fetch(`/bdsm/${sessionId}/mitspieler`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -267,13 +268,15 @@
spieltMit: p.spieltMit,
rollen: p.rollen,
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.`);
}
// 3. Aufgaben setzen
const aufgabenRes = await fetch(`/session/${sessionId}/aufgaben`, {
const aufgabenRes = await fetch(`/bdsm/${sessionId}/aufgaben`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(gameContent),
@@ -281,7 +284,7 @@
if (!aufgabenRes.ok) throw new Error('Aufgaben konnten nicht gespeichert werden.');
sessionStorage.setItem('bdsm-session-id', sessionId);
window.location.href = '/sessionbdsmingame.html';
window.location.href = '/bdsmingame.html';
} catch (e) {
showMessage(e.message, 'error');
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>
<!-- Tabs: Feed | Pinnwand | Lock-Historie -->
<!-- Tabs: Feed | Pinnwand | Spielhistorie -->
<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" 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>
<!-- Feed Tab (vorausgewählt) -->
@@ -429,10 +429,10 @@
<div id="pinnwandList"></div>
</div>
<!-- Lock-Historie Tab -->
<div class="profil-tab-panel" id="tab-lockhistory">
<div id="lockHistoryList" 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>
<!-- Spielhistorie Tab -->
<div class="profil-tab-panel" id="tab-gamehistory">
<div id="gameHistoryList" style="margin-top:0.75rem;"></div>
<p id="gameHistoryEmpty" style="color:var(--color-muted);font-size:0.9rem;display:none;">Keine abgeschlossenen Locks vorhanden.</p>
</div>
</div>
@@ -467,6 +467,7 @@
// ── State ──
const params = new URLSearchParams(window.location.search);
let targetUserId = params.get('userId');
const previewMode = params.get('preview'); // 'FREUND' | 'UNBEKANNT' | null
let myUserId = null;
let isOwnProfile = false;
let profileData = null;
@@ -524,25 +525,52 @@
}
myUserId = me ? me.userId : null;
isOwnProfile = me && me.userId === profile.userId;
isOwnProfile = !previewMode && me && me.userId === profile.userId;
profileData = profile;
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';
renderHeader(profile);
renderGallery();
loadFriends();
if (profile.beschreibung) {
// ── Galerie ──
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 = '';
const el = document.getElementById('profilBeschreibung');
el.style.display = '';
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 = '';
// Feed-Tab ist vorausgewählt → sofort laden
loadProfilPosts();
profilPostsObserver.observe(document.getElementById('profilPostsSentinel'));
// Feed-Tab ist vorausgewählt → sofort laden (nur wenn sichtbar)
if (canSee(profile.sichtbarkeitFeed, isFriend, isOwnProfile)) {
loadProfilPosts();
profilPostsObserver.observe(document.getElementById('profilPostsSentinel'));
}
} catch {
document.getElementById('loadingHint').textContent = 'Fehler beim Laden.';
document.getElementById('loadingHint').style.display = '';
@@ -568,14 +596,16 @@
d.innerHTML = `<span class="label">${label}</span><span class="value">${esc(value)}</span>`;
tags.appendChild(d);
};
if (profile.alter) addTag('Alter', profile.alter + ' J.');
if (profile.groesse) addTag('Größe', profile.groesse + ' cm');
if (profile.gewicht) addTag('Gewicht', profile.gewicht + ' kg');
if (profile.geschlecht) addTag('Geschlecht', GESCHLECHT_LABEL[profile.geschlecht] || profile.geschlecht);
if (profile.neigung) addTag('Neigung', NEIGUNG_LABEL[profile.neigung] || profile.neigung);
if (profile.beziehungsstatus) addTag('Beziehung', BEZIEHUNG_LABEL[profile.beziehungsstatus] || profile.beziehungsstatus);
if (profile.lockeeXp > 0) addTag('🔒 Lockee XP', profile.lockeeXp + ' XP');
if (profile.keyholderXp > 0) addTag('🔑 Keyholder XP', profile.keyholderXp + ' XP');
const grunddatenVisible = canSee(profile.sichtbarkeitGrunddaten, profile.friendStatus === 'FRIEND', isOwnProfile);
if (grunddatenVisible && profile.alter) addTag('Alter', profile.alter + ' J.');
if (grunddatenVisible && profile.groesse) addTag('Größe', profile.groesse + ' cm');
if (grunddatenVisible && profile.gewicht) addTag('Gewicht', profile.gewicht + ' kg');
if (grunddatenVisible && profile.geschlecht) addTag('Geschlecht', GESCHLECHT_LABEL[profile.geschlecht] || profile.geschlecht);
if (grunddatenVisible && profile.neigung) addTag('Neigung', NEIGUNG_LABEL[profile.neigung] || profile.neigung);
if (grunddatenVisible && profile.beziehungsstatus) addTag('Beziehung', BEZIEHUNG_LABEL[profile.beziehungsstatus] || profile.beziehungsstatus);
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
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 ──
function switchProfilTab(name, btn) {
document.querySelectorAll('.profil-tab-btn').forEach(b => b.classList.remove('active'));
@@ -777,47 +851,60 @@
</div>`;
}
// ── Lock-Historie ──
let lockHistoryLoaded = false;
async function loadLockHistory() {
if (lockHistoryLoaded) return;
lockHistoryLoaded = true;
const list = document.getElementById('lockHistoryList');
const empty = document.getElementById('lockHistoryEmpty');
// ── Spielhistorie ──
const GAME_TYPE_ICON = {
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>',
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>',
BDSM: '⛓️',
VANILLA: '❤️'
};
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>';
try {
const res = await fetch('/lockhistory?userId=' + targetUserId);
const res = await fetch('/gamehistory?userId=' + targetUserId);
if (!res.ok) { list.innerHTML = ''; return; }
const entries = await res.json();
list.innerHTML = '';
if (entries.length === 0) { empty.style.display = ''; return; }
list.innerHTML = entries.map(e => {
const icon = e.role === 'KEYHOLDER' ? '🔑' : '🔒';
const partner = e.role === 'KEYHOLDER'
? (e.lockeeName ? `<span style="color:var(--color-muted);font-size:0.78rem;">Lockee: ${esc(e.lockeeName)}</span>` : '')
: (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>');
const days = Math.floor(e.durationMinutes / 1440);
const hours = Math.floor((e.durationMinutes % 1440) / 60);
const mins = e.durationMinutes % 60;
const dur = days > 0
const gameIconRaw = GAME_TYPE_ICON[e.gameType] || '🎮';
const gameIcon = (e.gameType === 'CARDLOCK' || e.gameType === 'TIMELOCK')
? gameIconRaw
: `<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 mins = e.durationMinutes % 60;
const dur = days > 0
? `${days}d ${hours}h ${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;">`
: `<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>`;
return `<div style="display:flex;align-items:center;gap:0.75rem;padding:0.65rem 0;border-bottom:1px solid var(--color-secondary);">
<div style="position:relative;width:40px;height:40px;flex-shrink:0;">
${avatar}
<span style="position:absolute;top:-3px;left:-3px;font-size:1.4rem;line-height:1;">${icon}</span>
</div>
const participants = (e.participants || []).map(p => {
const badge = ROLE_BADGE[p.role] || '';
const img = p.picture
? `<img src="data:image/png;base64,${p.picture}" style="width:40px;height:40px;border-radius:50%;object-fit:cover;display:block;">`
: `<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>`;
return `<a href="/benutzer.html?userId=${esc(p.userId)}" style="position:relative;flex-shrink:0;text-decoration:none;" title="${esc(p.name || '')} (${p.role})">
${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="font-weight:600;font-size:0.92rem;">${esc(e.lockName) || 'Unbenanntes Lock'}</div>
<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-top:0.15rem;">
${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 style="font-weight:600;font-size:0.92rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${esc(e.gameName) || 'Unbenannt'}</div>
<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>
</div>
<div style="display:flex;gap:0.35rem;align-items:center;flex-shrink:0;">${participants}</div>
</div>`;
}).join('');
} catch(e) { list.innerHTML = ''; }
@@ -954,7 +1041,7 @@
: '◉';
const bildRaw = bilderCarousel(p.bilder);
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>';

View File

@@ -167,6 +167,33 @@
}
.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 */
.unlock-modal-bg {
display: none; position: fixed; inset: 0; z-index: 500;
@@ -214,6 +241,20 @@
</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 -->
<div class="lockee-dialog-bg" id="lockeeInviteDialog">
<div class="lockee-dialog-overlay" onclick="closeLockeeInviteDialog()"></div>
@@ -475,9 +516,29 @@
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 ──
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;
try {
const res = await fetch('/lockee/invitation/' + encodeURIComponent(token), { method: 'DELETE' });
@@ -487,7 +548,7 @@
}
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;
try {
const res = await fetch('/keyholder/invitations/mine/' + encodeURIComponent(token), { method: 'DELETE' });
@@ -498,10 +559,11 @@
// ── Aktionen: Gesendet ──
async function cancelSentInvitation(token, type, btn) {
const msg = type === 'lockee'
? 'Einladung zurückziehen? Das Lock wird gelöscht und der Lockee wird benachrichtigt.'
: 'Keyholder-Einladung zurückziehen? Der Keyholder wird benachrichtigt.';
if (!confirm(msg)) return;
const title = 'Einladung zurückziehen';
const text = type === 'lockee'
? 'Das Lock wird gelöscht und der Lockee wird benachrichtigt.'
: 'Der Keyholder wird benachrichtigt.';
if (!await showConfirm(title, text)) return;
btn.disabled = true;
const url = type === 'lockee'
? '/lockee/invitations/sent/' + encodeURIComponent(token)
@@ -634,7 +696,7 @@
async function declineLockeeInviteDialog() {
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');
declineBtn.disabled = true;
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