diff --git a/.metadata/.lock_info b/.metadata/.lock_info index 2cfd875..23b70f1 100644 --- a/.metadata/.lock_info +++ b/.metadata/.lock_info @@ -1,5 +1,5 @@ -#Wed Mar 25 21:32:43 CET 2026 +#Thu Mar 26 16:50:07 CET 2026 display=\:0 host=mario-mint -process-id=43084 +process-id=121337 user=mario diff --git a/.metadata/.log b/.metadata/.log index 9b36c3a..d52844f 100644 --- a/.metadata/.log +++ b/.metadata/.log @@ -819,3 +819,287 @@ Binding(CTRL+R, ,,true),null), org.eclipse.ui.defaultAcceleratorConfiguration, org.eclipse.debug.ui.console,,,system) + +!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-26 00:13:15.217 +!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS +!SESSION 2026-03-26 07:50:09.934 ----------------------------------------------- +eclipse.buildId=4.39.0.20260305-0817 +java.version=21.0.6 +java.vendor=Eclipse Adoptium +BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE +Framework arguments: -product org.eclipse.epp.package.java.product +Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product + +!ENTRY ch.qos.logback.classic 1 0 2026-03-26 07:50:11.470 +!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized. + +!ENTRY ch.qos.logback.classic 1 0 2026-03-26 07:50:14.978 +!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-26 07:50:15.138 +!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-26 07:50:15.138 +!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-26 07:50:15.273 +!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-26 07:50:15.273 +!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.debug.core 4 125 2026-03-26 07:52:41.947 +!MESSAGE Error logged from Debug Core: +!STACK 0 +java.io.IOException: Stream closed + at java.base/java.io.BufferedInputStream.getBufIfOpen(BufferedInputStream.java:188) + at java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:343) + at java.base/java.io.BufferedInputStream.implRead(BufferedInputStream.java:420) + at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:405) + at java.base/java.io.FilterInputStream.read(FilterInputStream.java:95) + at org.eclipse.debug.internal.core.OutputStreamMonitor.internalRead(OutputStreamMonitor.java:235) + at org.eclipse.debug.internal.core.OutputStreamMonitor.read(OutputStreamMonitor.java:211) + at java.base/java.lang.Thread.run(Thread.java:1583) + +!ENTRY org.eclipse.jface 2 0 2026-03-26 07:52:44.356 +!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. +!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-26 07:52:44.356 +!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.springframework.tooling.boot.ls 1 0 2026-03-26 10:28:33.777 +!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS +!SESSION 2026-03-26 10:32:16.891 ----------------------------------------------- +eclipse.buildId=4.39.0.20260305-0817 +java.version=21.0.6 +java.vendor=Eclipse Adoptium +BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE +Framework arguments: -product org.eclipse.epp.package.java.product +Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product + +!ENTRY ch.qos.logback.classic 1 0 2026-03-26 10:32:18.391 +!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized. + +!ENTRY ch.qos.logback.classic 1 0 2026-03-26 10:32:21.250 +!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-26 10:32:21.401 +!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-26 10:32:21.401 +!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-26 10:32:21.529 +!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-26 10:32:21.529 +!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory' + +!ENTRY org.eclipse.jface 2 0 2026-03-26 10:32:54.845 +!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. +!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-26 10:32:54.845 +!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.springframework.tooling.boot.ls 1 0 2026-03-26 10:47:57.323 +!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS +!SESSION 2026-03-26 11:31:28.220 ----------------------------------------------- +eclipse.buildId=4.39.0.20260305-0817 +java.version=21.0.6 +java.vendor=Eclipse Adoptium +BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE +Framework arguments: -product org.eclipse.epp.package.java.product +Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product + +!ENTRY ch.qos.logback.classic 1 0 2026-03-26 11:31:29.790 +!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized. + +!ENTRY ch.qos.logback.classic 1 0 2026-03-26 11:31:36.957 +!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-26 11:31:37.090 +!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-26 11:31:37.090 +!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-26 11:31:37.210 +!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-26 11:31:37.210 +!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory' +!SESSION 2026-03-26 16:50:01.920 ----------------------------------------------- +eclipse.buildId=4.39.0.20260305-0817 +java.version=21.0.6 +java.vendor=Eclipse Adoptium +BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE +Framework arguments: -product org.eclipse.epp.package.java.product +Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product + +!ENTRY ch.qos.logback.classic 1 0 2026-03-26 16:50:03.384 +!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized. + +!ENTRY ch.qos.logback.classic 1 0 2026-03-26 16:50:07.703 +!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-26 16:50:07.810 +!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-26 16:50:07.810 +!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-26 16:50:07.943 +!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-26 16:50:07.943 +!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.lsp4e 2 0 2026-03-26 16:55:47.890 +!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.jface 2 0 2026-03-26 19:21:16.922 +!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. +!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-26 19:21:16.922 +!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.jface 2 0 2026-03-26 20:13:11.892 +!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation. +!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-26 20:13:11.892 +!MESSAGE A conflict occurred for CTRL+R: +Binding(CTRL+R, + ParameterizedCommand(Command(org.eclipse.debug.ui.commands.RunToLine,Run to Line, + Resume and break when execution reaches the current line, + Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true), + WorkbenchHandlerServiceHandler("org.eclipse.debug.ui.commands.RunToLine"), + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.debug.ui.debugging,,,system) +Binding(CTRL+R, + ParameterizedCommand(Command(org.springframework.ide.eclipse.boot.restart.commands.restart,Trigger Restart, + Restart Spring Boot Application, + Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true), + WorkbenchHandlerServiceHandler("org.springframework.ide.eclipse.boot.restart.commands.restart"), + ,,true),null), + org.eclipse.ui.defaultAcceleratorConfiguration, + org.eclipse.debug.ui.console,,,system) + +!ENTRY Activator 4 0 2026-03-26 22:43:57.249 +!MESSAGE NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, boolean)" because "conf" is null +!STACK 0 +java.lang.NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, boolean)" because "conf" is null + at org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.getEnableJmx(BootLaunchConfigurationDelegate.java:426) + at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:195) + at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:1) + at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.updateOwnerStatesAndFireEvents(RunStateTracker.java:216) + at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.processCreated(RunStateTracker.java:273) + at org.springframework.ide.eclipse.boot.util.ProcessTracker.processCreated(ProcessTracker.java:105) + at org.springframework.ide.eclipse.boot.util.ProcessTracker.handleDebugEvent(ProcessTracker.java:71) + at org.springframework.ide.eclipse.boot.util.ProcessTracker$1.handleDebugEvents(ProcessTracker.java:48) + at org.eclipse.debug.core.DebugPlugin$EventNotifier.run(DebugPlugin.java:1422) + at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:47) + at org.eclipse.debug.core.DebugPlugin$EventNotifier.dispatch(DebugPlugin.java:1456) + at org.eclipse.debug.core.DebugPlugin$EventDispatchJob.run(DebugPlugin.java:521) + at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63) + +!ENTRY Activator 4 0 2026-03-26 22:43:57.250 +!MESSAGE NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, String)" because "conf" is null +!STACK 0 +java.lang.NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, String)" because "conf" is null + at org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate.getProjectName(AbstractBootLaunchConfigurationDelegate.java:322) + at org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate.getProject(AbstractBootLaunchConfigurationDelegate.java:307) + at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:196) + at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:1) + at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.updateOwnerStatesAndFireEvents(RunStateTracker.java:216) + at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.processCreated(RunStateTracker.java:273) + at org.springframework.ide.eclipse.boot.util.ProcessTracker.processCreated(ProcessTracker.java:105) + at org.springframework.ide.eclipse.boot.util.ProcessTracker.handleDebugEvent(ProcessTracker.java:71) + at org.springframework.ide.eclipse.boot.util.ProcessTracker$1.handleDebugEvents(ProcessTracker.java:48) + at org.eclipse.debug.core.DebugPlugin$EventNotifier.run(DebugPlugin.java:1422) + at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:47) + at org.eclipse.debug.core.DebugPlugin$EventNotifier.dispatch(DebugPlugin.java:1456) + at org.eclipse.debug.core.DebugPlugin$EventDispatchJob.run(DebugPlugin.java:521) + at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63) + +!ENTRY Activator 4 0 2026-03-26 22:43:57.399 +!MESSAGE NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, boolean)" because "conf" is null +!STACK 0 +java.lang.NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, boolean)" because "conf" is null + at org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.getEnableJmx(BootLaunchConfigurationDelegate.java:426) + at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:195) + at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:1) + at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.updateOwnerStatesAndFireEvents(RunStateTracker.java:216) + at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.processCreated(RunStateTracker.java:273) + at org.springframework.ide.eclipse.boot.util.ProcessTracker.processCreated(ProcessTracker.java:105) + at org.springframework.ide.eclipse.boot.util.ProcessTracker.handleDebugEvent(ProcessTracker.java:71) + at org.springframework.ide.eclipse.boot.util.ProcessTracker$1.handleDebugEvents(ProcessTracker.java:48) + at org.eclipse.debug.core.DebugPlugin$EventNotifier.run(DebugPlugin.java:1422) + at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:47) + at org.eclipse.debug.core.DebugPlugin$EventNotifier.dispatch(DebugPlugin.java:1456) + at org.eclipse.debug.core.DebugPlugin$EventDispatchJob.run(DebugPlugin.java:521) + at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63) + +!ENTRY Activator 4 0 2026-03-26 22:43:57.399 +!MESSAGE NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, String)" because "conf" is null +!STACK 0 +java.lang.NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, String)" because "conf" is null + at org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate.getProjectName(AbstractBootLaunchConfigurationDelegate.java:322) + at org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate.getProject(AbstractBootLaunchConfigurationDelegate.java:307) + at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:196) + at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:1) + at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.updateOwnerStatesAndFireEvents(RunStateTracker.java:216) + at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.processCreated(RunStateTracker.java:273) + at org.springframework.ide.eclipse.boot.util.ProcessTracker.processCreated(ProcessTracker.java:105) + at org.springframework.ide.eclipse.boot.util.ProcessTracker.handleDebugEvent(ProcessTracker.java:71) + at org.springframework.ide.eclipse.boot.util.ProcessTracker$1.handleDebugEvents(ProcessTracker.java:48) + at org.eclipse.debug.core.DebugPlugin$EventNotifier.run(DebugPlugin.java:1422) + at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:47) + at org.eclipse.debug.core.DebugPlugin$EventNotifier.dispatch(DebugPlugin.java:1456) + at org.eclipse.debug.core.DebugPlugin$EventDispatchJob.run(DebugPlugin.java:521) + at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63) diff --git a/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json b/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json index 243f1de..96eebdb 100644 --- a/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json +++ b/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json @@ -1,7 +1,7 @@ [ { - "version" : "9.5.0-20260325015243+0000", - "buildTime" : "20260325015243+0000", - "commitId" : "627839c6a3532eeab60306fd7b577d6f1c866ece", + "version" : "9.5.0-20260326015913+0000", + "buildTime" : "20260326015913+0000", + "commitId" : "b62b56136fe3f28a01c3e35f77694c3d5af75916", "current" : false, "snapshot" : true, "nightly" : false, @@ -10,15 +10,15 @@ "rcFor" : "", "milestoneFor" : "", "broken" : false, - "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-bin.zip", - "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-bin.zip.sha256", - "checksum" : "f031a868b2d9707fe07c78ad0888d4be5d6bb87e3f8a118ffbf3b0b497c32922", - "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-wrapper.jar.sha256", - "wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a" + "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260326015913+0000-bin.zip", + "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260326015913+0000-bin.zip.sha256", + "checksum" : "ace6a98f3a565a82cd108c6a115f64837cd5bb8e95d563c6e2dfb884ea3a8fe5", + "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260326015913+0000-wrapper.jar.sha256", + "wrapperChecksum" : "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7" }, { - "version" : "9.6.0-20260325005438+0000", - "buildTime" : "20260325005438+0000", - "commitId" : "4a2f60ed3e5db8c8eadc899d49da7c6abf7140ee", + "version" : "9.6.0-20260326003843+0000", + "buildTime" : "20260326003843+0000", + "commitId" : "f6b5714b236ea05298517d966a339045da81a5ee", "current" : false, "snapshot" : true, "nightly" : true, @@ -27,10 +27,10 @@ "rcFor" : "", "milestoneFor" : "", "broken" : false, - "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-bin.zip", - "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-bin.zip.sha256", - "checksum" : "1419ec3a9f2e924772188c709cd681d073c7c821dbd82beb9a6e8f6b78f7723b", - "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-wrapper.jar.sha256", + "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260326003843+0000-bin.zip", + "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260326003843+0000-bin.zip.sha256", + "checksum" : "9d70bda347d4cdbc4fc8ce8550d53ce5d1b2add847f4720e8543ff6c74c322b8", + "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260326003843+0000-wrapper.jar.sha256", "wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a" }, { "version" : "9.4.1", diff --git a/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources b/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources index 6a8ed2f..82d6fde 100644 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources and b/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources differ diff --git a/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi index e1e5bde..1c59fea 100644 --- a/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi +++ b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi @@ -1,8 +1,8 @@ - - + + activeSchemeId:org.eclipse.ui.defaultAcceleratorConfiguration - + @@ -11,9 +11,9 @@ topLevel shellMaximized - - - + + + persp.actionSet:org.eclipse.mylyn.tasks.ui.navigation persp.actionSet:org.eclipse.ui.cheatsheets.actionSet @@ -84,132 +84,131 @@ persp.editorOnboardingCommand:Show Key Assist$$$Shift+Ctrl+L persp.editorOnboardingCommand:New$$$Ctrl+N persp.editorOnboardingCommand:Open Type$$$Shift+Ctrl+T - - - - + + + + org.eclipse.e4.primaryNavigationStack - + active + noFocus + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:General - + View categoryTag:Java - + Minimized - + View categoryTag:Spring - - + + View categoryTag:Git - - - - + + + + org.eclipse.e4.secondaryNavigationStack - Minimized - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Mylyn - + View categoryTag:Java - + View categoryTag:Ant - + org.eclipse.e4.secondaryDataStack Oomph Gradle Debug Version Control (Team) - active - noFocus - + View categoryTag:General - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Terminal - + View categoryTag:Gradle - + View categoryTag:Gradle - + View categoryTag:Debug - + View categoryTag:Version Control (Team) - + View categoryTag:Oomph NoRestore @@ -218,7 +217,7 @@ - + persp.actionSet:org.eclipse.mylyn.tasks.ui.navigation persp.actionSet:org.eclipse.ui.cheatsheets.actionSet @@ -267,99 +266,99 @@ persp.editorOnboardingCommand:Step Over$$$F6 persp.editorOnboardingCommand:Step Return$$$F7 persp.editorOnboardingCommand:Resume$$$F8 - - + + org.eclipse.e4.primaryNavigationStack - + View categoryTag:Debug - + View categoryTag:General - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Java - - - - + + + + org.eclipse.e4.secondaryNavigationStack - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Ant - - + + View categoryTag:General - + View categoryTag:General - + View categoryTag:Debug - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Terminal - + View categoryTag:Debug - + View categoryTag:General @@ -368,2707 +367,2674 @@ - - + + View categoryTag:Help - + View categoryTag:General - + View categoryTag:Help - + View categoryTag:Help - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:Help - - + + EditorStack - - + + Editor removeOnHide - org.eclipse.ui.genericeditor.GenericEditor + org.eclipse.jdt.ui.ClassFileEditor - - + + Editor removeOnHide - org.eclipse.ui.genericeditor.GenericEditor - - - - Editor - removeOnHide - org.eclipse.ui.genericeditor.GenericEditor - - - - Editor - removeOnHide - org.eclipse.jdt.ui.CompilationUnitEditor - - - - Editor - removeOnHide - org.eclipse.jdt.ui.CompilationUnitEditor - - - - Editor - removeOnHide - org.eclipse.jdt.ui.CompilationUnitEditor - - - - Editor - removeOnHide - org.eclipse.ui.genericeditor.GenericEditor + org.eclipse.jdt.ui.ClassFileEditor - + View categoryTag:Java - + active + ViewMenu menuContribution:menu - + - + View categoryTag:Java - + View categoryTag:General - + - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:Java - + View categoryTag:Java - + - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:General - active - + ViewMenu menuContribution:menu - + - + View categoryTag:General - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:General - + ViewMenu menuContribution:menu - + - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Mylyn - + View categoryTag:Terminal - + View categoryTag:Java - + View categoryTag:Git - + View categoryTag:Java - + View categoryTag:Spring - + ViewMenu menuContribution:menu - + - + View categoryTag:Ant - + View categoryTag:Gradle - + ViewMenu menuContribution:menu - + - + View categoryTag:Gradle - + ViewMenu menuContribution:menu - + - + View categoryTag:Debug busy - + ViewMenu menuContribution:menu - + - + View categoryTag:Debug - + View categoryTag:Debug - + ViewMenu menuContribution:menu - + - + View categoryTag:Debug - + ViewMenu menuContribution:menu - + - + View categoryTag:Debug - + ViewMenu menuContribution:menu - + - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Debug - + ViewMenu menuContribution:menu - + - + View categoryTag:Version Control (Team) - + ViewMenu menuContribution:menu - + - + View categoryTag:Oomph NoRestore - + ViewMenu menuContribution:menu - + - - + + toolbarSeparator - + - + Draggable - + - + toolbarSeparator - + - + Draggable - - + + - + toolbarSeparator - + - + Draggable - + Draggable - + Draggable - + Draggable - + toolbarSeparator - + - + Draggable - + - + Draggable - - Draggable - - + toolbarSeparator - + - + toolbarSeparator - + - + Draggable - + stretch SHOW_RESTORE_MENU - + Draggable HIDEABLE SHOW_RESTORE_MENU - - + + stretch - + Draggable - + Draggable - - + + TrimStack Draggable - + TrimStack Draggable - + TrimStack Draggable - + TrimStack Draggable - - + + TrimStack Draggable - + TrimStack Draggable - + TrimStack Draggable - + TrimStack Draggable - + TrimStack Draggable - - - - - - - - - - - - - - + + + + + + + + + + + + + + platform:gtk - - - - + + + + platform:gtk - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - + + + + + - - + + - - - - - - - - - + + + + + + + + + - - + + - - - + + + - - - - - + + + + + - - + + - - - + + + - - - + + + - - - - - - - - + + + + + + + + platform:gtk - - - - - + + + + + - - + + - - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + + + + - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - + + - - - - - - - + + + + + + + - - - - + + + + - - - - - - - - + + + + + + + + - - + + - - - - - - + + + + + + - - - - - - + + + + + + - - + + - - - - - - - - + + + + + + + + - - - + + + - - - - + + + + - - + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Editor removeOnHide - + View categoryTag:Ant - + View categoryTag:Gradle - + View categoryTag:Gradle - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Debug - + View categoryTag:Java - + View categoryTag:Git - + View categoryTag:Git - + View categoryTag:Git - + View categoryTag:Git NoRestore - + View categoryTag:Git - + View categoryTag:Help - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Debug - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Java Browsing - + View categoryTag:Java Browsing - + View categoryTag:Java Browsing - + View categoryTag:Java Browsing - + View categoryTag:Java - + View categoryTag:General - + View categoryTag:Java - + View categoryTag:Java - + View categoryTag:Docker - + View categoryTag:Docker - + View categoryTag:Docker - + View categoryTag:Docker - + View categoryTag:Language Servers - + View categoryTag:Language Servers - + View categoryTag:Language Servers - + View categoryTag:Maven - + View categoryTag:Maven - + View categoryTag:Maven - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Mylyn - + View categoryTag:Oomph - + View categoryTag:Oomph NoRestore - + View categoryTag:Plug-in Development - + View categoryTag:General - + View categoryTag:Version Control (Team) - + View categoryTag:Version Control (Team) - + View categoryTag:Terminal - + View categoryTag:Help - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Help - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:General - + View categoryTag:Spring - + View categoryTag:Spring - + View categoryTag:Spring - - + + glue move_after:PerspectiveSpacer SHOW_RESTORE_MENU - + move_after:Spacer Glue HIDEABLE SHOW_RESTORE_MENU - + glue move_after:SearchField SHOW_RESTORE_MENU - - - - - + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - + + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - - - + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml index 38813f0..35ef23d 100644 --- a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml +++ b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml @@ -2,14 +2,16 @@ - - + - - + + + + + diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml index 2e7dc7c..868a6a3 100644 --- a/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml +++ b/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml @@ -28,4 +28,5 @@ + diff --git a/.metadata/.plugins/org.eclipse.m2e.logback/0.log b/.metadata/.plugins/org.eclipse.m2e.logback/0.log index 1815cf6..8d1096a 100644 --- a/.metadata/.plugins/org.eclipse.m2e.logback/0.log +++ b/.metadata/.plugins/org.eclipse.m2e.logback/0.log @@ -9,3 +9,7 @@ 2026-03-24 11:26:24,107 [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-25 07:26:14,133 [Worker-5: 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-25 21:32:47,427 [Worker-8: 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-26 07:50:17,235 [Worker-7: 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-26 10:32:24,614 [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-26 11:31:40,355 [Worker-8: 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-26 16:50:11,098 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. diff --git a/.metadata/version.ini b/.metadata/version.ini index 62840ca..1b8cc98 100644 --- a/.metadata/version.ini +++ b/.metadata/version.ini @@ -1,3 +1,3 @@ -#Wed Mar 25 21:32:43 CET 2026 +#Thu Mar 26 16:50:07 CET 2026 org.eclipse.core.runtime=2 org.eclipse.platform=4.39.0.v20260226-0420 diff --git a/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java b/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java index 4e54107..0872b8c 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java @@ -65,7 +65,9 @@ public class SecurityConfig { .requestMatchers("/admin.html").authenticated() .requestMatchers("/communityvotes.html").authenticated() .requestMatchers("/keyholder.html").authenticated() + .requestMatchers("/keyholder-finden.html").authenticated() .requestMatchers("/meine-locks.html").authenticated() + .requestMatchers("/entdecken-vorlagen.html").authenticated() .requestMatchers("/unlock-history.html").authenticated() .requestMatchers("/einladungen.html").authenticated() .requestMatchers("/joinlock.html").authenticated() diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardEnum.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardEnum.java index 901b36a..7851cbe 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardEnum.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardEnum.java @@ -47,16 +47,14 @@ public enum CardEnum { CUM { @Override public Card get() { - // TODO Auto-generated method stub - return null; + return new CumCard(); } }, CUM_IN_CAGE { @Override public Card get() { - // TODO Auto-generated method stub - return null; + return new CumInCageCard(); } }; diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java index 1af7dbf..110ea10 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java @@ -156,6 +156,9 @@ public class CardLockController { return ResponseEntity.badRequest().build(); var lockee = lockeeOpt.get(); + if (cardLockServiceFactory.hasActiveLock(req.lockeeUserId())) + return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists")); + LocalDateTime now = LocalDateTime.now(); CardLockEntity lock = new CardLockEntity(); lock.setName(req.name()); @@ -194,7 +197,7 @@ public class CardLockController { } // Self-lockee path (existing behavior) - if (cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId)) + if (cardLockServiceFactory.hasActiveLock(myId)) return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists")); LockControllType controllType = req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE; @@ -346,7 +349,7 @@ public class CardLockController { if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - String code = cardLockServiceFactory.create(l).endHygieneOpening(); + String code = cardLockServiceFactory.create(l).endTempOpening(); return ResponseEntity.ok(Map.of("newUnlockCode", code)); } @@ -421,11 +424,11 @@ public class CardLockController { var meOpt = userRepository.findByEmail(principal.getName()); if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); - var locks = cardlockRepository.findByLockee(meOpt.get().getUserId()); - var active = locks.stream().filter(l -> l.getUnlockTime() == null).findFirst(); - if (active.isEmpty()) + UUID myId = meOpt.get().getUserId(); + var activeLockId = cardLockServiceFactory.findActiveLockId(myId); + if (activeLockId.isEmpty()) return ResponseEntity.noContent().build(); - return ResponseEntity.ok(Map.of("lockId", active.get().getLockId().toString())); + return ResponseEntity.ok(Map.of("lockId", activeLockId.get().toString())); } @GetMapping("/cardlock/{lockId}") @@ -477,6 +480,11 @@ public class CardLockController { result.put("hygieneOpeningDue", hygieneOpeningDue); result.put("hygieneSecondsRemaining", hygieneSecondsRemaining); result.put("hygieneOpeningActive", l.getTempOpeningTime() != null && TempOpeningReason.HYGIENE == l.getTempOpeningReason()); + boolean tempOpeningActive = l.getTempOpeningTime() != null && TempOpeningReason.HYGIENE != l.getTempOpeningReason(); + result.put("tempOpeningActive", tempOpeningActive); + if (tempOpeningActive) { + result.put("tempOpeningUnlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : ""); + } result.put("hygieneOpeningStarted", l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : null); result.put("hygieneDurationMinutes", diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java index 1596549..d955035 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java @@ -11,7 +11,6 @@ import org.slf4j.LoggerFactory; import de.oaa.xxx.games.chastity.common.BaseLockEntity; import de.oaa.xxx.games.chastity.common.BaseLockService; -import de.oaa.xxx.games.chastity.common.CodeCreator; import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository; import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository; import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository; @@ -99,11 +98,13 @@ public class CardLockService extends BaseLockService implements LockControlCallb @Override protected void applyHygieneOvertime(Long overtime) { + LOGGER.debug("Apply {} Minutes Overtime"); if (lock.getFrozenUntil() != null) { lock.setFrozenUntil(lock.getFrozenUntil().plusMinutes(overtime * 4)); } else { lock.setFrozenUntil(LocalDateTime.now().plusMinutes(overtime * 4)); } + LOGGER.debug("Frozen until {}", lock.getFrozenUntil()); } // ── Card drawing ────────────────────────────────────────────────────────── @@ -251,31 +252,6 @@ public class CardLockService extends BaseLockService implements LockControlCallb return lock.getUnlockCode(); } - public String endCumming() { - Long overtime = calcOvertime(); - if (overtime != null) { - if (lock.getKeyholder() == null) { - applyHygieneOvertime(overtime); - } else { - reportKeyholder(overtime); - } - } - lock.setTempOpeningDuration(null); - lock.setTempOpeningTime(null); - lock.setTempOpeningReason(null); - - if (lockControl != null - && lock.getControllType() != de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE) { - lockControl.lock(); - cardLockRepository.save(lock); - return lock.getUnlockCode() != null ? lock.getUnlockCode() : ""; - } - var code = CodeCreator.createNumeric(lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5); - lock.setUnlockCode(code); - cardLockRepository.save(lock); - return code; - } - // ── Assigned task penalty ───────────────────────────────────────────────── public void applyAssignedTaskPenalty(AssignedTaskEntity task) { diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceFactory.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceFactory.java index 3881ca2..1cc5ade 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceFactory.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceFactory.java @@ -1,9 +1,14 @@ package de.oaa.xxx.games.chastity.cardlock; +import java.util.Optional; +import java.util.UUID; + import de.oaa.xxx.games.history.GameHistoryRepository; +import de.oaa.xxx.games.chastity.common.BaseLockService; import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository; import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository; import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository; +import de.oaa.xxx.games.chastity.timelock.TimeLockRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository; @@ -35,11 +40,14 @@ public class CardLockServiceFactory { private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository; private final CommunityTaskVoteRepository communityTaskVoteRepository; private final LockControlFactory lockControlFactory; + private final CardlockRepository cardlockRepository; + private final TimeLockRepository timeLockRepository; public CardLockServiceFactory( CommunityVerificationRepository communityVerificationRepository, CommunityVerificationVoteRepository communityVerificationVoteRepository, CardLockRepository cardLockRepository, + CardlockRepository cardlockRepository, GameHistoryRepository gameHistoryRepository, UserRepository userRepository, KeyholderNotificationRepository keyholderNotificationRepository, @@ -48,8 +56,10 @@ public class CardLockServiceFactory { SystemMessageService systemMessageService, KeyholderTaskChoiceRepository keyholderTaskChoiceRepository, CommunityTaskVoteRepository communityTaskVoteRepository, - LockControlFactory lockControlFactory) { + LockControlFactory lockControlFactory, + TimeLockRepository timeLockRepository) { this.cardLockRepository = cardLockRepository; + this.cardlockRepository = cardlockRepository; this.communityVerificationRepository = communityVerificationRepository; this.communityVerificationVoteRepository = communityVerificationVoteRepository; this.gameHistoryRepository = gameHistoryRepository; @@ -61,6 +71,19 @@ public class CardLockServiceFactory { this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository; this.communityTaskVoteRepository = communityTaskVoteRepository; this.lockControlFactory = lockControlFactory; + this.timeLockRepository = timeLockRepository; + } + + public boolean hasActiveLock(UUID lockeeId) { + return BaseLockService.hasActiveLock(lockeeId, cardlockRepository, timeLockRepository); + } + + public Optional findActiveLockId(UUID lockeeId) { + var cardLock = cardlockRepository.findByLockee(lockeeId).stream() + .filter(l -> l.getUnlockTime() == null).findFirst(); + if (cardLock.isPresent()) return Optional.of(cardLock.get().getLockId()); + return timeLockRepository.findFirstByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeId) + .map(l -> l.getLockId()); } /** diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java index 1031acf..476c011 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java @@ -12,10 +12,12 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import de.oaa.xxx.games.chastity.cardlock.CardlockRepository; import de.oaa.xxx.games.chastity.community.CommunityTaskVoteEntity; import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository; import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository; import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository; +import de.oaa.xxx.games.chastity.timelock.TimeLockRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationEntity; import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceEntity; @@ -91,6 +93,18 @@ public abstract class BaseLockService { this.communityTaskVoteRepository = communityTaskVoteRepository; } + // ── Lockee-Prüfung ──────────────────────────────────────────────────────── + + /** + * Prüft ob der Anwender bereits ein aktives Lock (CardLock oder TimeLock) als Lockee hat. + * Ein Lock gilt als aktiv wenn startTime gesetzt und unlockTime null ist. + */ + public static boolean hasActiveLock(UUID lockeeId, CardlockRepository cardlockRepo, + TimeLockRepository timelockRepo) { + return cardlockRepo.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeId) + || timelockRepo.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeId); + } + // ── Gemeinsame Hilfsmethoden ────────────────────────────────────────────── protected Long calcOvertime() { @@ -192,10 +206,10 @@ public abstract class BaseLockService { unlockCodeHistoryService.save(lock.getLockee(), lock.getLockId(), lock.getName(), lock.getUnlockCode(), reason.toString()); } - public String endHygieneOpening() { - BaseLockEntity lock = getLock(); - LocalDateTime now = LocalDateTime.now(); - Long overtime = calcOvertime(); + public String endTempOpening() { + var lock = getLock(); + var now = LocalDateTime.now(); + var overtime = calcOvertime(); if (overtime != null) { if (lock.getKeyholder() != null) { reportKeyholder(overtime); @@ -203,19 +217,19 @@ public abstract class BaseLockService { applyHygieneOvertime(overtime); } afterHygieneClosing(); - lock.setLastHygineOpening(now); + if (TempOpeningReason.HYGIENE == lock.getTempOpeningReason()) { + lock.setLastHygineOpening(now); + } + lock.setTempOpeningReason(null); lock.setTempOpeningDuration(null); lock.setTempOpeningTime(null); if (lockControl != null && lock.getControllType() != de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE) { - // TTLock/Trust: lockControl.lock() setzt PIN am Gerät (oder tut nichts bei Trust). - // Kein Software-Code notwendig. - lockControl.lock(); + lockControl.lock(); saveLock(); return lock.getUnlockCode() != null ? lock.getUnlockCode() : ""; } - // UNLOCK_CODE (oder kein lockControl): neuen numerischen Code generieren - String code = CodeCreator.createNumeric(lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5); + var code = CodeCreator.createNumeric(lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5); lock.setUnlockCode(code); saveLock(); return code; diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateController.java index b509cec..db5d63a 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateController.java @@ -45,6 +45,8 @@ public class BaseLockTemplateController { dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes()); dto.put("taskCount", t.getTasks() != null ? t.getTasks().size() : 0); dto.put("requiresVerification", t.isRequiresVerification()); + dto.put("published", t.isPublished()); + dto.put("showAuthor", t.isShowAuthor()); return dto; }).toList(); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateEntity.java index 71056eb..6b7205a 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateEntity.java @@ -47,7 +47,16 @@ public class BaseLockTemplateEntity { private boolean requiresVerification; @Column(nullable = false) private TaskMode taskMode = TaskMode.RANDOM; - + + @Column(nullable = false) + private boolean published = false; + + @Column(nullable = false) + private boolean showAuthor = false; + + @Column(nullable = false) + private long subscriberCount = 0; + public TaskMode getTaskCardMode() { return taskMode != null ? taskMode : TaskMode.RANDOM; } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateRepository.java index b99b23a..fa2a404 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockTemplateRepository.java @@ -7,7 +7,10 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -public interface BaseLockTemplateRepository extends JpaRepository{ +public interface BaseLockTemplateRepository extends JpaRepository { List findByOwner(UUID owner); + List findByOwnerAndPublishedTrue(UUID owner); Page findByOwner(UUID owner, Pageable pageable); + Page findByPublishedTrue(Pageable pageable); + Page findByPublishedTrueAndNameContainingIgnoreCase(String name, Pageable pageable); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/TemplateExploreController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/TemplateExploreController.java new file mode 100644 index 0000000..11ed445 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/TemplateExploreController.java @@ -0,0 +1,336 @@ +package de.oaa.xxx.games.chastity.common; + +import de.oaa.xxx.games.chastity.cardlock.CardlockTemplateEntity; +import de.oaa.xxx.games.chastity.cardlock.CardlockTemplateRepository; +import de.oaa.xxx.games.chastity.timelock.TimeLockTemplateEntity; +import de.oaa.xxx.games.chastity.timelock.TimeLockTemplateRepository; +import de.oaa.xxx.games.chastity.tasks.TaskMode; +import de.oaa.xxx.user.UserRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/templates") +public class TemplateExploreController { + + private final BaseLockTemplateRepository templateRepository; + private final TemplateSubscriptionRepository subscriptionRepository; + private final TimeLockTemplateRepository timeLockTemplateRepository; + private final CardlockTemplateRepository cardlockTemplateRepository; + private final UserRepository userRepository; + + public TemplateExploreController(BaseLockTemplateRepository templateRepository, + TemplateSubscriptionRepository subscriptionRepository, + TimeLockTemplateRepository timeLockTemplateRepository, + CardlockTemplateRepository cardlockTemplateRepository, + UserRepository userRepository) { + this.templateRepository = templateRepository; + this.subscriptionRepository = subscriptionRepository; + this.timeLockTemplateRepository = timeLockTemplateRepository; + this.cardlockTemplateRepository = cardlockTemplateRepository; + this.userRepository = userRepository; + } + + // ── Öffentliche Vorlagen entdecken ──────────────────────────────────────── + + @GetMapping("/public") + public ResponseEntity> getPublicTemplates( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "") String q, + Principal principal) { + + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + size = Math.min(size, 20); + var pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "subscriberCount")); + + Page pageResult = q.isBlank() + ? templateRepository.findByPublishedTrue(pageable) + : templateRepository.findByPublishedTrueAndNameContainingIgnoreCase(q.trim(), pageable); + + Set subscribedIds = subscriptionRepository.findByUserId(myId) + .stream().map(TemplateSubscriptionEntity::getTemplateId).collect(Collectors.toSet()); + + List> content = pageResult.getContent().stream() + .map(t -> toPublicDto(t, myId, subscribedIds)) + .toList(); + + return ResponseEntity.ok(Map.of( + "content", content, + "page", pageResult.getNumber(), + "hasMore", !pageResult.isLast() + )); + } + + // ── Einzelne Vorlage per ID (für Detail-Dialog) ────────────────────────── + + @GetMapping("/{id}/public") + public ResponseEntity> getTemplate( + @PathVariable UUID id, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var tOpt = templateRepository.findById(id); + if (tOpt.isEmpty()) return ResponseEntity.notFound().build(); + var t = tOpt.get(); + + // Sichtbar wenn: eigene Vorlage ODER veröffentlicht + if (!t.getOwner().equals(myId) && !t.isPublished()) return ResponseEntity.status(403).build(); + + Set subscribedIds = subscriptionRepository.findByUserId(myId) + .stream().map(TemplateSubscriptionEntity::getTemplateId).collect(Collectors.toSet()); + return ResponseEntity.ok(toPublicDto(t, myId, subscribedIds)); + } + + // ── Eigene Vorlagen (für Auswahl-Dropdown) ─────────────────────────────── + + @GetMapping("/mine") + public ResponseEntity>> getMyTemplates(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + List> result = templateRepository.findByOwner(myId).stream() + .map(t -> toPublicDto(t, myId, Set.of())) + .toList(); + return ResponseEntity.ok(result); + } + + // ── Abonnierte Vorlagen ─────────────────────────────────────────────────── + + @GetMapping("/subscribed") + public ResponseEntity>> getSubscribedTemplates(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + List subs = subscriptionRepository.findByUserId(myId); + Set subscribedIds = subs.stream().map(TemplateSubscriptionEntity::getTemplateId).collect(Collectors.toSet()); + + List> result = new ArrayList<>(); + for (var sub : subs) { + templateRepository.findById(sub.getTemplateId()).ifPresent(t -> { + if (t.isPublished()) { + result.add(toPublicDto(t, myId, subscribedIds)); + } + }); + } + return ResponseEntity.ok(result); + } + + // ── Abonnieren ──────────────────────────────────────────────────────────── + + @PostMapping("/{id}/subscribe") + @Transactional + public ResponseEntity subscribe(@PathVariable UUID id, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var tOpt = templateRepository.findById(id); + if (tOpt.isEmpty() || !tOpt.get().isPublished()) return ResponseEntity.notFound().build(); + if (tOpt.get().getOwner().equals(myId)) return ResponseEntity.status(409).build(); + if (subscriptionRepository.findByUserIdAndTemplateId(myId, id).isPresent()) + return ResponseEntity.status(409).build(); + + var sub = new TemplateSubscriptionEntity(); + sub.setUserId(myId); + sub.setTemplateId(id); + sub.setSubscribedAt(LocalDateTime.now()); + subscriptionRepository.save(sub); + + var t = tOpt.get(); + t.setSubscriberCount(t.getSubscriberCount() + 1); + templateRepository.save(t); + + return ResponseEntity.noContent().build(); + } + + // ── Abo kündigen ────────────────────────────────────────────────────────── + + @DeleteMapping("/{id}/subscribe") + @Transactional + public ResponseEntity unsubscribe(@PathVariable UUID id, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var subOpt = subscriptionRepository.findByUserIdAndTemplateId(myId, id); + if (subOpt.isEmpty()) return ResponseEntity.noContent().build(); + + subscriptionRepository.delete(subOpt.get()); + + templateRepository.findById(id).ifPresent(t -> { + t.setSubscriberCount(Math.max(0, t.getSubscriberCount() - 1)); + templateRepository.save(t); + }); + + return ResponseEntity.noContent().build(); + } + + // ── Kopie erstellen (Fork) ──────────────────────────────────────────────── + + @PostMapping("/{id}/fork") + @Transactional + public ResponseEntity> fork(@PathVariable UUID id, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var tOpt = templateRepository.findById(id); + if (tOpt.isEmpty() || !tOpt.get().isPublished()) return ResponseEntity.notFound().build(); + + var source = tOpt.get(); + String copyName = (source.getName() != null ? source.getName() : "Vorlage") + " (Kopie)"; + + UUID newId; + if (source instanceof TimeLockTemplateEntity tl) { + var copy = new TimeLockTemplateEntity(); + copy.setOwner(myId); + copy.setName(copyName); + copy.setMinTimeInMinutes(tl.getMinTimeInMinutes()); + copy.setMaxTimeInMinutes(tl.getMaxTimeInMinutes()); + copy.setEndTimeVisible(tl.isEndTimeVisible()); + copy.setHygineOpeningDurationMinutes(tl.getHygineOpeningDurationMinutes()); + copy.setHygineOpeningEveryMinites(tl.getHygineOpeningEveryMinites()); + copy.setTasks(tl.getTasks()); + copy.setTaskEveryMinutes(tl.getTaskEveryMinutes()); + copy.setMinTasksPerDay(tl.getMinTasksPerDay()); + copy.setSpinningWheelEntries(tl.getSpinningWheelEntries()); + copy.setSpinsEveryMinutes(tl.getSpinsEveryMinutes()); + copy.setMinSpinsPerDay(tl.getMinSpinsPerDay()); + copy.setRequiresVerification(tl.isRequiresVerification()); + copy.setTaskMode(tl.getTaskMode() != null ? tl.getTaskMode() : TaskMode.RANDOM); + copy.setPenaltyType(tl.getPenaltyType()); + copy.setPenaltyValue(tl.getPenaltyValue()); + newId = timeLockTemplateRepository.save(copy).getTemplateId(); + } else if (source instanceof CardlockTemplateEntity cl) { + var copy = new CardlockTemplateEntity(); + copy.setOwner(myId); + copy.setName(copyName); + copy.setCardCountsMin(cl.getCardCountsMin()); + copy.setCardCountsMax(cl.getCardCountsMax()); + copy.setPickEveryMinute(cl.getPickEveryMinute()); + copy.setAccumulatePicks(cl.isAccumulatePicks()); + copy.setShowRemainingCards(cl.isShowRemainingCards()); + copy.setHygineOpeningDurationMinutes(cl.getHygineOpeningDurationMinutes()); + copy.setHygineOpeningEveryMinites(cl.getHygineOpeningEveryMinites()); + copy.setTasks(cl.getTasks()); + copy.setRequiresVerification(cl.isRequiresVerification()); + copy.setTaskMode(cl.getTaskMode() != null ? cl.getTaskMode() : TaskMode.RANDOM); + newId = cardlockTemplateRepository.save(copy).getTemplateId(); + } else { + return ResponseEntity.status(500).build(); + } + + return ResponseEntity.ok(Map.of("templateId", newId.toString())); + } + + // ── Veröffentlichen ─────────────────────────────────────────────────────── + + @PatchMapping("/{id}/publish") + @Transactional + public ResponseEntity publish(@PathVariable UUID id, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + var me = meOpt.get(); + + var tOpt = templateRepository.findById(id); + if (tOpt.isEmpty()) return ResponseEntity.notFound().build(); + var t = tOpt.get(); + if (!t.getOwner().equals(me.getUserId())) return ResponseEntity.status(403).build(); + + t.setPublished(true); + t.setShowAuthor(me.isProfilBeiVeroeffentlichungenSichtbar()); + templateRepository.save(t); + return ResponseEntity.noContent().build(); + } + + // ── Veröffentlichung entfernen ──────────────────────────────────────────── + + @DeleteMapping("/{id}/publish") + @Transactional + public ResponseEntity unpublish(@PathVariable UUID id, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var tOpt = templateRepository.findById(id); + if (tOpt.isEmpty()) return ResponseEntity.notFound().build(); + var t = tOpt.get(); + if (!t.getOwner().equals(myId)) return ResponseEntity.status(403).build(); + + t.setPublished(false); + t.setShowAuthor(false); + t.setSubscriberCount(0); + templateRepository.save(t); + subscriptionRepository.deleteByTemplateId(id); + return ResponseEntity.noContent().build(); + } + + // ── DTO Helper ──────────────────────────────────────────────────────────── + + private Map toPublicDto(BaseLockTemplateEntity t, UUID myId, Set subscribedIds) { + boolean isOwn = t.getOwner().equals(myId); + boolean isSubscribed = subscribedIds.contains(t.getTemplateId()); + + String authorName = null; + String authorProfilePicture = null; + if (t.isShowAuthor()) { + var authorOpt = userRepository.findById(t.getOwner()); + authorName = authorOpt.map(u -> u.getName()).orElse(null); + authorProfilePicture = authorOpt.map(u -> u.getProfilePicture()).orElse(null); + } + + Map dto = new LinkedHashMap<>(); + dto.put("templateId", t.getTemplateId().toString()); + dto.put("lockType", t instanceof TimeLockTemplateEntity ? "TIMELOCK" : "CARDLOCK"); + dto.put("name", t.getName() != null ? t.getName() : ""); + dto.put("subscriberCount", t.getSubscriberCount()); + dto.put("authorName", authorName); + dto.put("authorProfilePicture", authorProfilePicture); + dto.put("isOwnTemplate", isOwn); + dto.put("isSubscribed", isSubscribed); + dto.put("taskCount", t.getTasks() != null ? t.getTasks().size() : 0); + dto.put("requiresVerification", t.isRequiresVerification()); + dto.put("hygieneEnabled", t.getHygineOpeningEveryMinites() != null); + dto.put("hygineOpeningEveryMinites", t.getHygineOpeningEveryMinites()); + dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes()); + dto.put("tasks", t.getTasks() != null ? t.getTasks() : List.of()); + dto.put("taskMode", t.getTaskMode() != null ? t.getTaskMode().name() : "RANDOM"); + + if (t instanceof TimeLockTemplateEntity tl) { + dto.put("minTimeInMinutes", tl.getMinTimeInMinutes()); + dto.put("maxTimeInMinutes", tl.getMaxTimeInMinutes()); + dto.put("endTimeVisible", tl.isEndTimeVisible()); + dto.put("taskEveryMinutes", tl.getTaskEveryMinutes()); + dto.put("minTasksPerDay", tl.getMinTasksPerDay()); + dto.put("spinningWheelEntries", tl.getSpinningWheelEntries() != null ? tl.getSpinningWheelEntries() : List.of()); + dto.put("spinsEveryMinutes", tl.getSpinsEveryMinutes()); + dto.put("minSpinsPerDay", tl.getMinSpinsPerDay()); + dto.put("penaltyType", tl.getPenaltyType() != null ? tl.getPenaltyType().name() : null); + dto.put("penaltyValue", tl.getPenaltyValue()); + } else if (t instanceof CardlockTemplateEntity cl) { + dto.put("cardCountsMin", cl.getCardCountsMin() != null ? cl.getCardCountsMin() : Map.of()); + dto.put("cardCountsMax", cl.getCardCountsMax() != null ? cl.getCardCountsMax() : Map.of()); + dto.put("pickEveryMinute", cl.getPickEveryMinute()); + dto.put("accumulatePicks", cl.isAccumulatePicks()); + dto.put("showRemainingCards",cl.isShowRemainingCards()); + } + + return dto; + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/TemplateSubscriptionEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/TemplateSubscriptionEntity.java new file mode 100644 index 0000000..1b31db7 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/TemplateSubscriptionEntity.java @@ -0,0 +1,33 @@ +package de.oaa.xxx.games.chastity.common; + +import java.time.LocalDateTime; +import java.util.UUID; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +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 = "template_subscription") +public class TemplateSubscriptionEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(nullable = false) + private UUID userId; + + @Column(nullable = false) + private UUID templateId; + + @Column(nullable = false) + private LocalDateTime subscribedAt; +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/TemplateSubscriptionRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/TemplateSubscriptionRepository.java new file mode 100644 index 0000000..a9311fe --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/common/TemplateSubscriptionRepository.java @@ -0,0 +1,18 @@ +package de.oaa.xxx.games.chastity.common; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; + +public interface TemplateSubscriptionRepository extends JpaRepository { + Optional findByUserIdAndTemplateId(UUID userId, UUID templateId); + List findByUserId(UUID userId); + long countByTemplateId(UUID templateId); + @Transactional + void deleteByTemplateId(UUID templateId); + @Transactional + void deleteByUserIdAndTemplateId(UUID userId, UUID templateId); +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferController.java new file mode 100644 index 0000000..f9f2117 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferController.java @@ -0,0 +1,384 @@ +package de.oaa.xxx.games.chastity.keyholder; + +import de.oaa.xxx.games.chastity.cardlock.*; +import de.oaa.xxx.games.chastity.common.*; +import de.oaa.xxx.games.chastity.lockcontroll.LockControllType; +import de.oaa.xxx.games.chastity.tasks.TaskMode; +import de.oaa.xxx.games.chastity.timelock.*; +import de.oaa.xxx.social.SystemMessageService; +import de.oaa.xxx.social.entity.MessageCause; +import de.oaa.xxx.subscription.SubscriptionLimitService; +import de.oaa.xxx.user.Geschlecht; +import de.oaa.xxx.user.UserRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.time.LocalDateTime; +import java.util.*; + +@RestController +@RequestMapping("/keyholder-offers") +public class KeyholderOfferController { + + private final KeyholderOfferRepository offerRepository; + private final BaseLockTemplateRepository templateRepository; + private final TemplateSubscriptionRepository subscriptionRepository; + private final UserRepository userRepository; + private final CardlockRepository cardlockRepository; + private final TimeLockRepository timeLockRepository; + private final CardLockServiceFactory cardLockServiceFactory; + private final TimeLockServiceFactory timeLockServiceFactory; + private final KeyholderInvitationRepository invitationRepository; + private final SystemMessageService systemMessageService; + private final SubscriptionLimitService subscriptionLimitService; + + public KeyholderOfferController( + KeyholderOfferRepository offerRepository, + BaseLockTemplateRepository templateRepository, + TemplateSubscriptionRepository subscriptionRepository, + UserRepository userRepository, + CardlockRepository cardlockRepository, + TimeLockRepository timeLockRepository, + CardLockServiceFactory cardLockServiceFactory, + TimeLockServiceFactory timeLockServiceFactory, + KeyholderInvitationRepository invitationRepository, + SystemMessageService systemMessageService, + SubscriptionLimitService subscriptionLimitService) { + this.offerRepository = offerRepository; + this.templateRepository = templateRepository; + this.subscriptionRepository = subscriptionRepository; + this.userRepository = userRepository; + this.cardlockRepository = cardlockRepository; + this.timeLockRepository = timeLockRepository; + this.cardLockServiceFactory = cardLockServiceFactory; + this.timeLockServiceFactory = timeLockServiceFactory; + this.invitationRepository = invitationRepository; + this.systemMessageService = systemMessageService; + this.subscriptionLimitService = subscriptionLimitService; + } + + // ── Eigene Angebote ─────────────────────────────────────────────────────── + + @GetMapping("/mine") + public ResponseEntity>> getMyOffers(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + List> result = offerRepository.findByOffererId(myId).stream() + .map(this::toDto) + .toList(); + return ResponseEntity.ok(result); + } + + record CreateOfferRequest(UUID templateId, List targetGenders, boolean directStart) {} + + @PostMapping + @Transactional + public ResponseEntity> createOffer( + @RequestBody CreateOfferRequest req, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + // Limit prüfen + long existing = offerRepository.countByOffererId(myId); + if (existing >= subscriptionLimitService.maxKeyholderOffers(myId)) { + return ResponseEntity.status(403).body(Map.of("error", "offer_limit_reached")); + } + + // Template muss dem User gehören oder abonniert sein + var tOpt = templateRepository.findById(req.templateId()); + if (tOpt.isEmpty()) return ResponseEntity.badRequest().build(); + var t = tOpt.get(); + + boolean isOwn = t.getOwner().equals(myId); + boolean isSubscribed = subscriptionRepository.findByUserIdAndTemplateId(myId, req.templateId()).isPresent(); + if (!isOwn && !isSubscribed) return ResponseEntity.status(403).build(); + + String genders = req.targetGenders() != null + ? String.join(",", req.targetGenders()) + : ""; + + String templateType = t instanceof TimeLockTemplateEntity ? "TIMELOCK" : "CARDLOCK"; + + KeyholderOfferEntity offer = new KeyholderOfferEntity(); + offer.setOffererId(myId); + offer.setTemplateId(req.templateId()); + offer.setTemplateName(t.getName() != null ? t.getName() : "Unbenannt"); + offer.setTemplateType(templateType); + offer.setTargetGenders(genders); + offer.setDirectStart(req.directStart()); + offer.setAcceptanceCount(0); + offer.setCreatedAt(LocalDateTime.now()); + offerRepository.save(offer); + + return ResponseEntity.ok(toDto(offer)); + } + + @PatchMapping("/{id}") + @Transactional + public ResponseEntity> updateOffer( + @PathVariable UUID id, + @RequestBody CreateOfferRequest req, + Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var offerOpt = offerRepository.findById(id); + if (offerOpt.isEmpty()) return ResponseEntity.notFound().build(); + var offer = offerOpt.get(); + if (!offer.getOffererId().equals(myId)) return ResponseEntity.status(403).build(); + + // Template darf geändert werden – muss dem User gehören oder abonniert sein + var tOpt = templateRepository.findById(req.templateId()); + if (tOpt.isEmpty()) return ResponseEntity.badRequest().build(); + var t = tOpt.get(); + boolean isOwn = t.getOwner().equals(myId); + boolean isSubscribed = subscriptionRepository.findByUserIdAndTemplateId(myId, req.templateId()).isPresent(); + if (!isOwn && !isSubscribed) return ResponseEntity.status(403).build(); + + String templateType = t instanceof TimeLockTemplateEntity ? "TIMELOCK" : "CARDLOCK"; + String genders = req.targetGenders() != null ? String.join(",", req.targetGenders()) : ""; + + offer.setTemplateId(req.templateId()); + offer.setTemplateName(t.getName() != null ? t.getName() : "Unbenannt"); + offer.setTemplateType(templateType); + offer.setTargetGenders(genders); + offer.setDirectStart(req.directStart()); + offerRepository.save(offer); + + return ResponseEntity.ok(toDto(offer)); + } + + @DeleteMapping("/{id}") + @Transactional + public ResponseEntity deleteOffer(@PathVariable UUID id, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var offerOpt = offerRepository.findById(id); + if (offerOpt.isEmpty()) return ResponseEntity.notFound().build(); + if (!offerOpt.get().getOffererId().equals(myId)) return ResponseEntity.status(403).build(); + + offerRepository.delete(offerOpt.get()); + return ResponseEntity.noContent().build(); + } + + // ── Angebote eines bestimmten Nutzers (für Profilseite) ────────────────── + + @GetMapping("/user/{userId}") + public ResponseEntity>> getOffersForUser( + @PathVariable UUID userId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + List> result = offerRepository.findByOffererId(userId).stream() + .map(o -> toPublicDto(o, myId)) + .toList(); + return ResponseEntity.ok(result); + } + + // ── Öffentliche Angebotsübersicht ───────────────────────────────────────── + + @GetMapping("/public") + public ResponseEntity>> getPublicOffers(Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + var me = meOpt.get(); + + String userGender = me.getGeschlecht() != null ? me.getGeschlecht().name() : null; + + List> result = offerRepository.findAllByOrderByAcceptanceCountDesc().stream() + .filter(o -> matchesGender(o, userGender)) + .map(o -> toPublicDto(o, me.getUserId())) + .toList(); + + return ResponseEntity.ok(result); + } + + private boolean matchesGender(KeyholderOfferEntity o, String userGender) { + String tg = o.getTargetGenders(); + if (tg == null || tg.isBlank()) return true; // alle Geschlechter + if (userGender == null) return false; // User ohne Geschlecht: nur unrestricted + return Arrays.asList(tg.split(",")).contains(userGender); + } + + // ── Angebot annehmen (Lock erstellen) ───────────────────────────────────── + + record JoinOfferRequest(LockControllType controllType, Integer unlockCodeLength) {} + + @PostMapping("/{id}/join") + @Transactional + public ResponseEntity> joinOffer( + @PathVariable UUID id, + @RequestBody JoinOfferRequest req, + Principal principal) { + + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + var offerOpt = offerRepository.findById(id); + if (offerOpt.isEmpty()) return ResponseEntity.notFound().build(); + var offer = offerOpt.get(); + + if (offer.getOffererId().equals(myId)) + return ResponseEntity.status(409).body(Map.of("error", "own_offer")); + + // Aktives Lock prüfen + if (cardLockServiceFactory.hasActiveLock(myId)) + return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists")); + + var tOpt = templateRepository.findById(offer.getTemplateId()); + if (tOpt.isEmpty()) return ResponseEntity.status(410).body(Map.of("error", "template_gone")); + var template = tOpt.get(); + + var offererOpt = userRepository.findById(offer.getOffererId()); + if (offererOpt.isEmpty()) return ResponseEntity.status(410).body(Map.of("error", "offerer_gone")); + var offerer = offererOpt.get(); + + LockControllType controllType = req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE; + int codeLen = (req.unlockCodeLength() != null && req.unlockCodeLength() >= 1) ? req.unlockCodeLength() : 5; + boolean directStart = offer.isDirectStart(); + UUID keyholderIdIfDirect = directStart ? offer.getOffererId() : null; + + UUID lockId; + String unlockCode = null; + + if (template instanceof TimeLockTemplateEntity tl) { + TimeLockAdditionalSettings settings = new TimeLockAdditionalSettings( + controllType, myId, keyholderIdIfDirect, false, codeLen); + TimeLockEntity lock = new TimeLockEntity(); + timeLockServiceFactory.create(lock).init(tl, settings); + timeLockRepository.save(lock); + lockId = lock.getLockId(); + unlockCode = lock.getUnlockCode(); + + } else if (template instanceof CardlockTemplateEntity cl) { + List cards = buildCardList(cl); + if (cards.isEmpty()) return ResponseEntity.badRequest().body(Map.of("error", "empty_deck")); + + CardLockEntity lock = new CardLockEntity(); + lock.setName(template.getName()); + lock.setLockee(myId); + lock.setKeyholder(keyholderIdIfDirect); + lock.setInitialCards(cards); + lock.setPickEveryMinute(cl.getPickEveryMinute() != null ? cl.getPickEveryMinute() : 60); + lock.setAccumulatePicks(cl.isAccumulatePicks()); + lock.setShowRemainingCards(cl.isShowRemainingCards()); + lock.setHygineOpeningDurationMinutes(cl.getHygineOpeningDurationMinutes()); + lock.setHygineOpeningEveryMinites(cl.getHygineOpeningEveryMinites()); + lock.setTasks(cl.getTasks() != null ? cl.getTasks() : List.of()); + lock.setRequiresVerification(cl.isRequiresVerification()); + lock.setTestLock(false); + lock.setTaskMode(cl.getTaskMode() != null ? cl.getTaskMode() : TaskMode.RANDOM); + lock.setUnlockCodeLength(codeLen); + lock.setControllType(controllType); + + LocalDateTime now = LocalDateTime.now(); + lock.setStartTime(now); + lock.setAvailableCards(new ArrayList<>(cards)); + lock.setOpenPicks(0); + lock.setNextCardIn(now.plusMinutes(lock.getPickEveryMinute())); + if (cl.getHygineOpeningEveryMinites() != null) { + lock.setLastHygineOpening(now); + } + cardlockRepository.save(lock); + + CardLockService initService = cardLockServiceFactory.create(lock); + if (initService.getLockControl() != null) { + initService.getLockControl().lock(); + } else { + lock.setUnlockCode(CodeCreator.createNumeric(codeLen)); + cardlockRepository.save(lock); + } + lockId = lock.getLockId(); + unlockCode = lock.getUnlockCode(); + } else { + return ResponseEntity.status(500).build(); + } + + String lockName = template.getName() != null ? template.getName() : "Unbenanntes Lock"; + boolean invitationSent = false; + if (!directStart) { + // Normaler Einladungsworkflow: Keyholder muss bestätigen + KeyholderInvitationEntity inv = new KeyholderInvitationEntity(); + inv.setLockId(lockId); + inv.setKeyholderUserId(offer.getOffererId()); + inv.setLockeeUserId(myId); + inv.setToken(UUID.randomUUID().toString().replace("-", "")); + inv.setCreatedAt(LocalDateTime.now()); + invitationRepository.save(inv); + + systemMessageService.send(myId, offerer.getUserId(), + me.getName() + " möchte dein Keyholder-Angebot annehmen und lädt dich als Keyholder für „" + + lockName + "\" ein.", + "/einladungen.html", MessageCause.INVITATION); + invitationSent = true; + } else { + // Direktstart: Keyholder wird direkt gesetzt, aber trotzdem benachrichtigen + systemMessageService.send(myId, offerer.getUserId(), + me.getName() + " hat dein Keyholder-Angebot angenommen und das Lock „" + + lockName + "\" gestartet.", + "/keyholder.html", MessageCause.INVITATION); + } + + // Annahmezähler erhöhen + offer.setAcceptanceCount(offer.getAcceptanceCount() + 1); + offerRepository.save(offer); + + Map response = new LinkedHashMap<>(); + response.put("lockId", lockId.toString()); + response.put("invitationSent", invitationSent); + if (unlockCode != null) response.put("unlockCode", unlockCode); + return ResponseEntity.ok(response); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private List buildCardList(CardlockTemplateEntity cl) { + List cards = new ArrayList<>(); + Map counts = cl.getCardCountsMax(); + if (counts == null) return cards; + counts.forEach((type, count) -> { + if (count != null && count > 0) { + try { + CardEnum card = CardEnum.valueOf(type); + for (int i = 0; i < count; i++) cards.add(card); + } catch (IllegalArgumentException ignored) {} + } + }); + return cards; + } + + private Map toDto(KeyholderOfferEntity o) { + Map dto = new LinkedHashMap<>(); + dto.put("id", o.getId().toString()); + dto.put("templateId", o.getTemplateId().toString()); + dto.put("templateName", o.getTemplateName()); + dto.put("templateType", o.getTemplateType()); + dto.put("targetGenders", o.getTargetGenders() != null ? Arrays.asList(o.getTargetGenders().split(",")).stream().filter(s -> !s.isBlank()).toList() : List.of()); + dto.put("directStart", o.isDirectStart()); + dto.put("acceptanceCount", o.getAcceptanceCount()); + dto.put("createdAt", o.getCreatedAt().toString()); + return dto; + } + + private Map toPublicDto(KeyholderOfferEntity o, UUID myId) { + Map dto = toDto(o); + dto.put("isOwn", o.getOffererId().equals(myId)); + userRepository.findById(o.getOffererId()).ifPresent(u -> { + dto.put("offererName", u.getName()); + dto.put("offererProfilePic", u.getProfilePicture()); + }); + return dto; + } +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferEntity.java new file mode 100644 index 0000000..ce81ec7 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferEntity.java @@ -0,0 +1,48 @@ +package de.oaa.xxx.games.chastity.keyholder; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +@Entity +@Table(name = "keyholder_offer") +public class KeyholderOfferEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column + private UUID id; + + @Column(nullable = false) + private UUID offererId; + + @Column(nullable = false) + private UUID templateId; + + /** Denormalisiert für schnelle Anzeige */ + @Column(length = 200) + private String templateName; + + /** "CARDLOCK" oder "TIMELOCK" – denormalisiert */ + @Column(length = 20) + private String templateType; + + /** Kommagetrennte Geschlechter: z.B. "WEIBLICH,DIVERS" – leer = alle */ + @Column(length = 100) + private String targetGenders; + + /** true = Lock wird sofort gestartet; false = Keyholder muss erst bestätigen */ + @Column(nullable = false) + private boolean directStart; + + @Column(nullable = false) + private int acceptanceCount = 0; + + @Column(nullable = false) + private LocalDateTime createdAt; +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferRepository.java new file mode 100644 index 0000000..a74d775 --- /dev/null +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferRepository.java @@ -0,0 +1,12 @@ +package de.oaa.xxx.games.chastity.keyholder; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.UUID; + +public interface KeyholderOfferRepository extends JpaRepository { + List findByOffererId(UUID offererId); + long countByOffererId(UUID offererId); + List findAllByOrderByAcceptanceCountDesc(); +} diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.java index c54f178..c1f3672 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.java @@ -257,7 +257,7 @@ public class LockeeInvitationController { unlockCode = CodeCreator.createNumeric(codeLines); int unlockMinutes = randomBetween(timeLock.getMinTimeInMinutes(), timeLock.getMaxTimeInMinutes()); timeLock.setStartTime(now); - timeLock.setUnlockTime(now.plusMinutes(unlockMinutes)); + timeLock.setEstimatedUnlockTime(now.plusMinutes(unlockMinutes)); timeLock.setUnlockCode(unlockCode); timeLock.setUnlockCodeLength(codeLines); if (timeLock.getHygineOpeningEveryMinites() != null) { diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java index 03cfff4..7cc08b1 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java @@ -22,6 +22,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -120,6 +121,9 @@ public class TimeLockController { if (lockeeOpt.isEmpty()) return ResponseEntity.badRequest().build(); var lockee = lockeeOpt.get(); + if (timeLockServiceFactory.hasActiveLock(req.lockeeUserId())) + return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists")); + TimeLockEntity lock = buildBaseEntity(template, myId, req.lockeeUserId(), false); lock.setStartTime(null); timeLockRepository.save(lock); @@ -144,7 +148,7 @@ public class TimeLockController { "lockeeInvitationSent", true)); } - if (timeLockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId)) + if (timeLockServiceFactory.hasActiveLock(myId)) return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists")); LockControllType controllType = req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE; @@ -223,7 +227,7 @@ public class TimeLockController { timeLockRepository.save(l); } - boolean timeUp = l.getUnlockTime() != null && l.getUnlockTime().isBefore(now); + boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now); boolean isFrozen = l.getFrozenFrom() != null && (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now)); @@ -319,7 +323,7 @@ public class TimeLockController { // Only expose unlock time if end time is visible OR time is up if (l.isEndTimeVisible() || timeUp) { - result.put("unlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : null); + result.put("unlockTime", l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : null); } else { result.put("unlockTime", null); } @@ -360,11 +364,14 @@ public class TimeLockController { }); } + result.put("controllType", l.getControllType() != null ? l.getControllType().name() : "UNLOCK_CODE"); result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock()); if (l.isKeyholderRequestedUnlock() || l.isTestLock() || timeUp) { result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : ""); } result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null); + result.put("emergencyAutoUnlocked", l.isEmergencyAutoUnlocked()); + result.put("actualUnlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : null); return ResponseEntity.ok(result); } @@ -382,7 +389,7 @@ public class TimeLockController { if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); var l = lockOpt.get(); if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); - if (l.getUnlockTime() == null) return ResponseEntity.status(409).build(); // not started + if (l.getEstimatedUnlockTime() == null) return ResponseEntity.status(409).build(); // not started if (l.getSpinningWheelEntries() == null || l.getSpinningWheelEntries().isEmpty()) return ResponseEntity.status(409).build(); @@ -414,7 +421,7 @@ public class TimeLockController { result.put("intVal", entry.getIntVal()); result.put("stringVal", entry.getStringVal()); // Include updated lock time fields - result.put("newUnlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : null); + result.put("newUnlockTime", l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : null); result.put("newFrozenUntil", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null); result.put("isFrozen", l.getFrozenFrom() != null); result.put("currentTask", l.getCurrentTask()); @@ -505,11 +512,11 @@ public class TimeLockController { return ResponseEntity.status(409).build(); TimeLockService service = timeLockServiceFactory.create(l); - String newCode = service.endHygieneOpening(); + String newCode = service.endTempOpening(); return ResponseEntity.ok(Map.of( "newUnlockCode", newCode, - "newUnlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : "")); + "newUnlockTime", l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : "")); } // ── Verifikation starten ───────────────────────────────────────────────────── @@ -576,7 +583,7 @@ public class TimeLockController { if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); LocalDateTime now = LocalDateTime.now(); - boolean timeUp = l.getUnlockTime() != null && l.getUnlockTime().isBefore(now); + boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now); boolean isFrozen = l.getFrozenFrom() != null && (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now)); @@ -604,6 +611,30 @@ public class TimeLockController { return ResponseEntity.noContent().build(); } + // ── Tatsächliche Entsperrzeit setzen ───────────────────────────────────────── + + @PatchMapping("/timelock/{lockId}/unlock-time") + @Transactional + public ResponseEntity setActualUnlockTime(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + UUID myId = meOpt.get().getUserId(); + + var lockOpt = timeLockRepository.findById(lockId); + if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); + + LocalDateTime now = LocalDateTime.now(); + boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now); + if (!timeUp && !l.isTestLock() && !l.isKeyholderRequestedUnlock()) + return ResponseEntity.status(409).build(); + + l.setUnlockTime(now); + timeLockRepository.save(l); + return ResponseEntity.noContent().build(); + } + // ── Keyholder-Ansicht ───────────────────────────────────────────────────────── @GetMapping("/timelock/as-keyholder") @@ -653,7 +684,7 @@ public class TimeLockController { var lockee = lockeeOpt.get(); LocalDateTime now = LocalDateTime.now(); - boolean timeUp = l.getUnlockTime() != null && l.getUnlockTime().isBefore(now); + boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now); boolean isFrozen = l.getFrozenFrom() != null && (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now)); @@ -689,7 +720,7 @@ public class TimeLockController { result.put("lockeeProfilePic", lockee.getProfilePicture()); result.put("startTime", l.getStartTime() != null ? l.getStartTime().toString() : null); result.put("unlockTime", (l.isEndTimeVisible() || timeUp) - ? (l.getUnlockTime() != null ? l.getUnlockTime().toString() : null) : null); + ? (l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : null) : null); result.put("timeUp", timeUp); result.put("isFrozen", isFrozen); result.put("frozenUntil", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null); @@ -721,6 +752,73 @@ public class TimeLockController { return ResponseEntity.noContent().build(); } + // ── Einfrieren (Keyholder) ──────────────────────────────────────────────────── + + record FreezeRequest(String frozenUntil) {} + + @PostMapping("/timelock/as-keyholder/{lockId}/freeze") + @Transactional + public ResponseEntity freezeTimeLock(@PathVariable UUID lockId, + @RequestBody FreezeRequest req, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + var lockOpt = timeLockRepository.findById(lockId); + if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build(); + + LocalDateTime until; + try { + until = LocalDateTime.parse(req.frozenUntil()); + } catch (Exception e) { + return ResponseEntity.badRequest().body(Map.of("error", "Ungültiges Datumsformat.")); + } + if (!until.isAfter(LocalDateTime.now())) + return ResponseEntity.badRequest().body(Map.of("error", "Zeitpunkt muss in der Zukunft liegen.")); + + l.setFrozenFrom(LocalDateTime.now()); + l.setFrozenUntil(until); + timeLockRepository.save(l); + + systemMessageService.send(myId, l.getLockee(), + me.getName() + " hat dein Lock bis " + + until.toLocalDate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")) + " " + + until.toLocalTime().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm")) + + " Uhr eingefroren.", + "/activetimelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/timelock/as-keyholder/{lockId}/freeze") + @Transactional + public ResponseEntity unfreezeTimeLock(@PathVariable UUID lockId, Principal principal) { + var meOpt = userRepository.findByEmail(principal.getName()); + if (meOpt.isEmpty()) return ResponseEntity.status(401).build(); + var me = meOpt.get(); + UUID myId = me.getUserId(); + + var lockOpt = timeLockRepository.findById(lockId); + if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); + var l = lockOpt.get(); + if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build(); + + TimeLockService service = timeLockServiceFactory.create(l); + service.unfreeze(); + l.setFrozenFrom(null); + l.setFrozenUntil(null); + timeLockRepository.save(l); + + systemMessageService.send(myId, l.getLockee(), + me.getName() + " hat dein Lock entfroren.", + "/activetimelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE); + + return ResponseEntity.noContent().build(); + } + // ── Notfall-Entsperrung ─────────────────────────────────────────────────────── @PostMapping("/timelock/{lockId}/emergency-unlock") @@ -800,4 +898,5 @@ public class TimeLockController { ImageIO.write(scaled, "jpeg", out); return out.toByteArray(); } + } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.java index c9773a6..bed085d 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.java @@ -46,7 +46,10 @@ public class TimeLockEntity extends BaseLockEntity { @Column private Integer penaltyValue; - @Column + @Column + private LocalDateTime estimatedUnlockTime; + + @Column private LocalDateTime frozenFrom; @Column diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockRepository.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockRepository.java index a8f0310..8e437f9 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockRepository.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockRepository.java @@ -11,6 +11,8 @@ public interface TimeLockRepository extends JpaRepository boolean existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID lockee); + java.util.Optional findFirstByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID lockee); + List findByKeyholderAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID keyholder); @Modifying(clearAutomatically = true) diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java index 9692001..e0e079b 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java @@ -92,7 +92,9 @@ public class TimeLockService extends BaseLockService implements LockControlCallb @Override protected void applyHygieneOvertime(Long overtime) { - lock.setUnlockTime(lock.getUnlockTime().plusMinutes(overtime * 4)); + LOGGER.debug("Apply {} Minutes Overtime"); + lock.setEstimatedUnlockTime(lock.getEstimatedUnlockTime().plusMinutes(overtime * 4)); + LOGGER.debug("New estimated endtime {}", lock.getEstimatedUnlockTime()); } // ── Hook overrides ──────────────────────────────────────────────────────── @@ -135,7 +137,7 @@ public class TimeLockService extends BaseLockService implements LockControlCallb int unlockTimeMinutes = (minMinutes != null && minMinutes < maxMinutes) ? minMinutes + new Random().nextInt(maxMinutes - minMinutes) : maxMinutes; - lock.setUnlockTime(now.plusMinutes(unlockTimeMinutes)); + lock.setEstimatedUnlockTime(now.plusMinutes(unlockTimeMinutes)); lock.setEndTimeVisible(template.isEndTimeVisible()); lock.setTasks(template.getTasks()); @@ -178,12 +180,12 @@ public class TimeLockService extends BaseLockService implements LockControlCallb public void addTime(Integer intVal) { LOGGER.debug("Lock addTime: %s minutes", intVal); - lock.setUnlockTime(lock.getUnlockTime().plusMinutes(intVal)); + lock.setEstimatedUnlockTime(lock.getEstimatedUnlockTime().plusMinutes(intVal)); } public void removeTime(Integer intVal) { LOGGER.debug("Lock removeTime: %s minutes", intVal); - lock.setUnlockTime(lock.getUnlockTime().minusMinutes(intVal)); + lock.setEstimatedUnlockTime(lock.getEstimatedUnlockTime().minusMinutes(intVal)); } public void freeze(Integer intVal) { @@ -202,7 +204,7 @@ public class TimeLockService extends BaseLockService implements LockControlCallb var unfreeTime = lock.getFrozenUntil() != null ? lock.getFrozenUntil() : LocalDateTime.now(); var diff = ChronoUnit.MINUTES.between(lock.getFrozenFrom(), unfreeTime); LOGGER.debug("Lock unfrozen - adding %s minutes to the lock", diff); - lock.setUnlockTime(lock.getUnlockTime().plusMinutes(diff)); + lock.setEstimatedUnlockTime(lock.getEstimatedUnlockTime().plusMinutes(diff)); } else { LOGGER.debug("Lock not frozen - ignore Call"); } @@ -358,7 +360,6 @@ public class TimeLockService extends BaseLockService implements LockControlCallb // ── Hygiene opening ─────────────────────────────────────────────────────── public void startHygieneOpening() { - lockControl.unlock(); startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes()); } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockServiceFactory.java b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockServiceFactory.java index 7498303..0905faf 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockServiceFactory.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockServiceFactory.java @@ -1,7 +1,11 @@ package de.oaa.xxx.games.chastity.timelock; +import java.util.UUID; + import org.springframework.stereotype.Service; +import de.oaa.xxx.games.chastity.cardlock.CardlockRepository; +import de.oaa.xxx.games.chastity.common.BaseLockService; import de.oaa.xxx.games.chastity.community.CommunityPilloryRepository; import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository; import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository; @@ -32,6 +36,7 @@ public class TimeLockServiceFactory { private final SystemMessageService systemMessageService; private CommunityVerificationVoteRepository communityVerificationVoteRepository; private final LockControlFactory lockControlFactory; + private final CardlockRepository cardlockRepository; public TimeLockServiceFactory(CommunityVerificationRepository verificationRepository, CommunityVerificationVoteRepository verificationVoteRepository, TimeLockRepository timeLockRepository, @@ -41,7 +46,7 @@ public class TimeLockServiceFactory { KeyholderVerificationRepository keyholderVerificationRepository, CommunityTaskVoteRepository communityTaskVoteRepository, CommunityPilloryRepository pilloryRepository, UnlockCodeHistoryService unlockCodeHistoryService, SystemMessageService systemMessageService, - LockControlFactory lockControlFactory) { + LockControlFactory lockControlFactory, CardlockRepository cardlockRepository) { this.communityVerificationVoteRepository = verificationVoteRepository; this.timeLockRepository = timeLockRepository; this.communityVerificationRepository = verificationRepository; @@ -55,6 +60,11 @@ public class TimeLockServiceFactory { this.communityTaskVoteRepository = communityTaskVoteRepository; this.keyholderVerificationRepository = keyholderVerificationRepository; this.lockControlFactory = lockControlFactory; + this.cardlockRepository = cardlockRepository; + } + + public boolean hasActiveLock(UUID lockeeId) { + return BaseLockService.hasActiveLock(lockeeId, cardlockRepository, timeLockRepository); } /** diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/SocialController.java b/xxxthegame/src/main/java/de/oaa/xxx/social/SocialController.java index 636a622..a2ab5ef 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/social/SocialController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/SocialController.java @@ -365,7 +365,8 @@ public class SocialController { user.getSichtbarkeitFeed(), user.getSichtbarkeitPinnwand(), user.getSichtbarkeitXp(), - user.getSichtbarkeitLockhistorie()); + user.getSichtbarkeitLockhistorie(), + user.isProfilBeiVeroeffentlichungenSichtbar()); } private MessageDto toMessageDto(MessageEntity m) { diff --git a/xxxthegame/src/main/java/de/oaa/xxx/social/dto/UserProfile.java b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/UserProfile.java index 8fcebbc..efcbc17 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/social/dto/UserProfile.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/social/dto/UserProfile.java @@ -30,12 +30,13 @@ public record UserProfile( Sichtbarkeit sichtbarkeitFeed, Sichtbarkeit sichtbarkeitPinnwand, Sichtbarkeit sichtbarkeitXp, - Sichtbarkeit sichtbarkeitLockhistorie + Sichtbarkeit sichtbarkeitLockhistorie, + boolean profilBeiVeroeffentlichungenSichtbar ) { /** 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, 0, - null, null, null, null, null, null, null); + null, null, null, null, null, null, null, false); } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/subscription/SubscriptionLimitService.java b/xxxthegame/src/main/java/de/oaa/xxx/subscription/SubscriptionLimitService.java index 11c2534..dbe75fc 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/subscription/SubscriptionLimitService.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/subscription/SubscriptionLimitService.java @@ -14,10 +14,11 @@ import java.util.UUID; public class SubscriptionLimitService { // ── Limits for STANDARD (no active subscription) ── - public static final int STANDARD_MAX_LOCK_TEMPLATES = 6; - public static final int STANDARD_MAX_TASK_GROUPS = 6; - public static final int STANDARD_MAX_TASKS_PER_GROUP = 50; - public static final int STANDARD_MAX_TOYS = 10; + public static final int STANDARD_MAX_LOCK_TEMPLATES = 6; + public static final int STANDARD_MAX_TASK_GROUPS = 6; + public static final int STANDARD_MAX_TASKS_PER_GROUP = 50; + public static final int STANDARD_MAX_TOYS = 10; + public static final int STANDARD_MAX_KEYHOLDER_OFFERS = 5; private final UserSubscriptionRepository subscriptionRepository; @@ -61,4 +62,10 @@ public class SubscriptionLimitService { if (hasActivePaidSubscription(userId)) return Integer.MAX_VALUE; return STANDARD_MAX_TOYS; } + + /** Max keyholder offers the user may create. */ + public int maxKeyholderOffers(UUID userId) { + if (hasActivePaidSubscription(userId)) return Integer.MAX_VALUE; + return STANDARD_MAX_KEYHOLDER_OFFERS; + } } diff --git a/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java b/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java index 0f5c0ae..2a5852d 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/UserController.java @@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController; import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity; import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository; import de.oaa.xxx.games.chastity.common.BaseLockRepository; +import de.oaa.xxx.games.chastity.common.BaseLockTemplateRepository; import de.oaa.xxx.games.chastity.common.CodeCreator; import de.oaa.xxx.games.chastity.ttlock.TTAuthService; import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository; @@ -57,6 +58,7 @@ public class UserController { private final TTAuthService ttAuthService; private final TTLockService ttLockService; private final BaseLockRepository baseLockRepository; + private final BaseLockTemplateRepository baseLockTemplateRepository; public UserController(UserRepository userRepository, RegistrationRepository registrationRepository, @@ -67,7 +69,8 @@ public class UserController { TTLockConfigRepository ttLockConfigRepository, TTAuthService ttAuthService, TTLockService ttLockService, - BaseLockRepository baseLockRepository) { + BaseLockRepository baseLockRepository, + BaseLockTemplateRepository baseLockTemplateRepository) { this.userRepository = userRepository; this.registrationRepository = registrationRepository; this.notificationPreferenceRepository = notificationPreferenceRepository; @@ -78,6 +81,7 @@ public class UserController { this.ttAuthService = ttAuthService; this.ttLockService = ttLockService; this.baseLockRepository = baseLockRepository; + this.baseLockTemplateRepository = baseLockTemplateRepository; } record ProfilePictureRequest(String picture, String pictureHq) {} @@ -94,7 +98,8 @@ public class UserController { Sichtbarkeit sichtbarkeitFeed, Sichtbarkeit sichtbarkeitPinnwand, Sichtbarkeit sichtbarkeitXp, - Sichtbarkeit sichtbarkeitLockhistorie) {} + Sichtbarkeit sichtbarkeitLockhistorie, + Boolean profilBeiVeroeffentlichungenSichtbar) {} @PutMapping("/me/picture") public ResponseEntity updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) { @@ -138,6 +143,16 @@ public class UserController { if (request.sichtbarkeitPinnwand() != null) user.setSichtbarkeitPinnwand(request.sichtbarkeitPinnwand()); if (request.sichtbarkeitXp() != null) user.setSichtbarkeitXp(request.sichtbarkeitXp()); if (request.sichtbarkeitLockhistorie()!= null) user.setSichtbarkeitLockhistorie(request.sichtbarkeitLockhistorie()); + if (request.profilBeiVeroeffentlichungenSichtbar() != null) { + boolean showAuthor = request.profilBeiVeroeffentlichungenSichtbar(); + user.setProfilBeiVeroeffentlichungenSichtbar(showAuthor); + // Alle veröffentlichten Templates synchronisieren + var templates = baseLockTemplateRepository.findByOwnerAndPublishedTrue(user.getUserId()); + for (var t : templates) { + t.setShowAuthor(showAuthor); + } + baseLockTemplateRepository.saveAll(templates); + } userRepository.save(user); LOGGER.info("User {} hat Datenschutz-Einstellungen aktualisiert", user.getUserId()); return ResponseEntity.ok().build(); diff --git a/xxxthegame/src/main/java/de/oaa/xxx/user/UserEntity.java b/xxxthegame/src/main/java/de/oaa/xxx/user/UserEntity.java index f7be127..7a9f574 100644 --- a/xxxthegame/src/main/java/de/oaa/xxx/user/UserEntity.java +++ b/xxxthegame/src/main/java/de/oaa/xxx/user/UserEntity.java @@ -92,6 +92,9 @@ public class UserEntity { @Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'") private Sichtbarkeit sichtbarkeitLockhistorie = Sichtbarkeit.ALLE; + @Column(nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") + private boolean profilBeiVeroeffentlichungenSichtbar = false; + public Integer getAlter() { return geburtsdatum != null ? Period.between(geburtsdatum, LocalDate.now()).getYears() : null; } diff --git a/xxxthegame/src/main/resources/static/activelock.html b/xxxthegame/src/main/resources/static/activelock.html index 2fd03ed..331f94e 100644 --- a/xxxthegame/src/main/resources/static/activelock.html +++ b/xxxthegame/src/main/resources/static/activelock.html @@ -678,6 +678,29 @@ + +
+
+
🔓
+

Lock geöffnet

+ + +
+

Dein aktueller Entsperrcode:

+
+ +
+ + + +
+
+
@@ -811,6 +834,7 @@ renderNextCardPanel(lock); renderHygienePanel(lock); renderVerificationPanel(lock); + renderTempOpeningPanel(lock); renderCardsPanel(lock); if (lock.keyholderRequestedUnlock) { @@ -1508,6 +1532,111 @@ }, 1000); } + // ── Temp-Öffnung (CARD / TASK / etc.) ── + + function renderTempOpeningPanel(lock) { + if (lock.tempOpeningActive) { + document.getElementById('tempOpeningCode').textContent = lock.tempOpeningUnlockCode || '–'; + document.getElementById('tempPhase1').style.display = ''; + document.getElementById('tempPhase2').style.display = 'none'; + document.querySelector('#tempOpeningModal h3').textContent = 'Lock geöffnet'; + document.getElementById('tempOpeningModal').classList.add('open'); + } else { + document.getElementById('tempOpeningModal').classList.remove('open'); + } + } + + async function endTempOpeningFlow() { + const isTtlock = _currentLock && _currentLock.controllType === 'TTLOCK'; + const isTrust = _currentLock && _currentLock.controllType === 'TRUST'; + + if (isTtlock) { + document.getElementById('tempOpeningModal').classList.remove('open'); + document.getElementById('ttlLoadingModal').classList.add('open'); + } + + const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/end', { method: 'POST' }); + + if (isTtlock) { + document.getElementById('ttlLoadingModal').classList.remove('open'); + if (!res.ok) { alert('Fehler beim Beenden der Öffnung.'); return; } + loadLock(); + return; + } + + if (!res.ok) { alert('Fehler beim Beenden der Öffnung.'); return; } + const data = await res.json(); + + if (isTrust || !data.newUnlockCode) { + document.getElementById('tempOpeningModal').classList.remove('open'); + loadLock(); + return; + } + + // UNLOCK_CODE: neuen Code anzeigen + document.getElementById('tempPhase1').style.display = 'none'; + document.getElementById('tempPhase2').style.display = 'flex'; + document.getElementById('tempNewCode').textContent = data.newUnlockCode; + document.getElementById('tempPhase2Hint').style.display = ''; + document.getElementById('tempPhase2Btn').textContent = 'OK'; + document.getElementById('tempPhase2Btn').onclick = startTempScramble; + document.getElementById('tempScrambleCountdown').style.display = 'none'; + document.querySelector('#tempOpeningModal h3').textContent = 'Lock geöffnet'; + } + + function closeTempOpeningModal() { + document.getElementById('tempOpeningModal').classList.remove('open'); + if (tempScrambleTimer) { clearInterval(tempScrambleTimer); tempScrambleTimer = null; } + if (tempScrambleCd) { clearInterval(tempScrambleCd); tempScrambleCd = null; } + loadLock(); + } + + let tempScrambleTimer = null; + let tempScrambleCd = null; + + function startTempScramble() { + const codeEl = document.getElementById('tempNewCode'); + const hintEl = document.getElementById('tempPhase2Hint'); + const cdEl = document.getElementById('tempScrambleCountdown'); + const btnEl = document.getElementById('tempPhase2Btn'); + const realCode = codeEl.textContent; + const len = realCode.length; + const DURATION = 3 * 60; + let remaining = DURATION; + let stopped = false; + + function randomCode() { + return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join(''); + } + function finish() { + stopped = true; + clearInterval(tempScrambleTimer); tempScrambleTimer = null; + clearInterval(tempScrambleCd); tempScrambleCd = null; + closeTempOpeningModal(); + } + + hintEl.style.display = 'none'; + cdEl.style.display = ''; + document.querySelector('#tempOpeningModal h3').textContent = 'Nun vergessen wir den Code…'; + btnEl.textContent = 'Abbrechen'; + btnEl.onclick = finish; + + function updateCd() { + const m = Math.floor(remaining / 60); + const s = remaining % 60; + cdEl.textContent = `${m}:${String(s).padStart(2,'0')}`; + } + updateCd(); + + tempScrambleTimer = setInterval(() => { if (!stopped) codeEl.textContent = randomCode(); }, 1000); + tempScrambleCd = setInterval(() => { + if (stopped) return; + remaining--; + updateCd(); + if (remaining <= 0) finish(); + }, 1000); + } + // ── Lock beenden ── function lockBeendenFragen() { document.getElementById('warnModalUnlockCode').textContent = _currentLock ? (_currentLock.unlockCode || '–') : '–'; diff --git a/xxxthegame/src/main/resources/static/activetimelock.html b/xxxthegame/src/main/resources/static/activetimelock.html index eac9b3f..c2b316d 100644 --- a/xxxthegame/src/main/resources/static/activetimelock.html +++ b/xxxthegame/src/main/resources/static/activetimelock.html @@ -183,21 +183,14 @@ } #wheelCanvas { border-radius: 50%; display: block; } - /* ── Spin-Result-Modal ── */ - .spin-modal-backdrop { - display: none; position: fixed; inset: 0; - background: rgba(0,0,0,0.65); z-index: 500; - align-items: center; justify-content: center; - } - .spin-modal-backdrop.open { display: flex; } - .spin-modal-box { - background: var(--color-card); border: 1px solid var(--color-secondary); - border-radius: 14px; padding: 2rem 1.75rem 1.5rem; max-width: 340px; width: 90%; + /* ── Glücksrad-Ergebnis (unterhalb des Rads) ── */ + .wheel-result { + margin-top: 1.25rem; width: 290px; display: flex; flex-direction: column; align-items: center; - gap: 1rem; text-align: center; + gap: 0.6rem; text-align: center; } .spin-result-icon { font-size: 3rem; } - .spin-result-title { font-size: 1.1rem; font-weight: 700; margin: 0; } + .spin-result-title { font-size: 1.1rem; font-weight: 700; margin: 0; color: var(--color-text); } .spin-result-desc { font-size: 0.9rem; color: var(--color-muted); line-height: 1.5; margin: 0; } /* ── Hygiene-Modal ── */ @@ -268,6 +261,17 @@ border-radius: 8px; cursor: pointer; width: auto; transition: background 0.15s; } .btn-lock-unlock:hover { background: rgba(46,204,113,0.28); } + .btn-confirm-unlock { + background: rgba(52,152,219,0.15); border: 1px solid rgba(52,152,219,0.45); + color: #3498db; font-size: 0.88rem; font-weight: 600; padding: 0.55rem 1.25rem; + border-radius: 8px; cursor: pointer; width: auto; transition: background 0.15s; + } + .btn-confirm-unlock:hover { background: rgba(52,152,219,0.28); } + .unlock-confirmed-badge { + font-size: 0.82rem; color: #2ecc71; + border: 1px solid rgba(46,204,113,0.35); border-radius: 7px; + padding: 0.35rem 0.75rem; display: inline-block; + } .btn-lock-beenden { background: transparent; border: 1px solid rgba(200,50,50,0.45); color: rgba(200,50,50,0.7); font-size: 0.82rem; padding: 0.5rem 1rem; @@ -368,16 +372,12 @@
-
-
- - -
-
-
-

-

- +
@@ -403,6 +403,12 @@
+ + @@ -430,6 +436,19 @@ + +
+
+
🔓
+

Lock entsperrt

+
+
+ + +
+
+
+
@@ -637,6 +656,7 @@ }; function startWheelSpin() { + document.getElementById('wheelResult').style.display = 'none'; document.getElementById('wheelAnimModal').classList.add('open'); wheelAngle = Math.random() * 2 * Math.PI; wheelResult = null; @@ -673,9 +693,7 @@ wheelAnimState = 'done'; drawWheelFrame(canvas, wheelAngle); setTimeout(() => { - document.getElementById('wheelAnimModal').classList.remove('open'); showSpinResult(wheelResult); - loadLock(); }, 750); return; } @@ -777,7 +795,7 @@ panel.style.display = ''; if (lock.spinDue) { - cdEl.textContent = 'Jetzt fällig'; + cdEl.textContent = 'Bereit'; cdEl.style.color = '#2ecc71'; btn.disabled = lock.isFrozen || lock.hygieneOpeningActive || false; return; @@ -792,7 +810,7 @@ function tick() { const diff = target - Date.now(); if (diff <= 0) { - cdEl.textContent = 'Jetzt fällig'; + cdEl.textContent = 'Bereit'; cdEl.style.color = '#2ecc71'; btn.disabled = lock.isFrozen || false; clearInterval(spinTickInterval); spinTickInterval = null; @@ -857,11 +875,13 @@ document.getElementById('spinResultIcon').textContent = info.icon; document.getElementById('spinResultTitle').textContent = info.title; document.getElementById('spinResultDesc').textContent = info.descFn(result); - document.getElementById('spinModal').classList.add('open'); + document.getElementById('wheelResult').style.display = ''; } function closeSpinModal() { - document.getElementById('spinModal').classList.remove('open'); + document.getElementById('wheelAnimModal').classList.remove('open'); + document.getElementById('wheelResult').style.display = 'none'; + loadLock(); } // ── Aufgaben-Panel ───────────────────────────────────────────────────────── @@ -992,10 +1012,25 @@ async function endHygieneOpening() { if (hygieneTickInterval) { clearInterval(hygieneTickInterval); hygieneTickInterval = null; } + if (_currentLock && _currentLock.controllType === 'TTLOCK') { + document.getElementById('hygienePhase1').style.display = 'none'; + document.getElementById('hygienePhase3').style.display = 'flex'; + } + const res = await fetch('/keyholder/timelock/' + lockId + '/hygiene/end', { method: 'POST' }); - if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; } + if (!res.ok) { + document.getElementById('hygienePhase3').style.display = 'none'; + document.getElementById('hygienePhase1').style.display = 'flex'; + alert('Fehler beim Beenden der Hygiene-Öffnung.'); + return; + } const data = await res.json(); + if (_currentLock && _currentLock.controllType === 'TTLOCK') { + closeHygieneModal(); + return; + } + document.getElementById('hygienePhase1').style.display = 'none'; const phase2 = document.getElementById('hygienePhase2'); phase2.style.display = 'flex'; @@ -1169,9 +1204,32 @@ if (!area) return; if (lock.timeUp) { - // Unlock-Button anzeigen + if (!lock.actualUnlockTime) { + area.innerHTML = ``; + } else { + area.innerHTML = ``; + } + } else if (lock.keyholderRequestedUnlock) { + const code = lock.unlockCode || '–'; + const isEmergency = lock.emergencyAutoUnlocked; + const title = isEmergency + ? '🆘 Lock automatisch freigegeben' + : '🔓 Dein Keyholder hat das Lock freigegeben!'; + const note = isEmergency + ? `
+ Da dein Keyholder nicht innerhalb einer Stunde reagiert hat, wurde das Lock automatisch geöffnet. Es werden keine XP vergeben und kein Eintrag in die Historie gespeichert. +
` + : ''; area.innerHTML = ` - `; +
+
${title}
+
Dein Entsperrcode
+
${code}
+ ${note} + +
`; } else if (lock.testLock) { area.innerHTML = ``; } else if (lock.emergencyUnlockRequested) { @@ -1188,11 +1246,40 @@ } } - function lockOeffnen() { - const code = _currentLock ? (_currentLock.unlockCode || '') : ''; - document.getElementById('warnModalUnlockCode').textContent = code || '(wird nach Bestätigung angezeigt)'; - document.getElementById('warnModal').classList.add('open'); - document.querySelector('#warnModal h3').textContent = '🔓 Lock öffnen?'; + async function confirmUnlock() { + const btn = document.querySelector('#lockActionArea .btn-lock-unlock'); + if (btn) btn.disabled = true; + try { + const res = await fetch('/keyholder/timelock/' + lockId + '/unlock-time', { method: 'PATCH' }); + if (res.ok || res.status === 204) { + const r2 = await fetch('/keyholder/timelock/' + lockId); + if (r2.ok) _currentLock = await r2.json(); + renderLockActionArea(_currentLock); + openUnlockResultModal(); + } else { + if (btn) btn.disabled = false; + } + } catch(_) { + if (btn) btn.disabled = false; + } + } + + function openUnlockResultModal() { + const lock = _currentLock; + const body = document.getElementById('unlockResultBody'); + if (lock && lock.controllType === 'TRUST') { + body.innerHTML = `

✓ Du kannst dich jetzt befreien.

`; + } else { + const code = (lock && lock.unlockCode) || '–'; + body.innerHTML = ` +
+
Entsperrcode
+
${code}
+
`; + } + document.getElementById('unlockResultModal').classList.add('open'); } function lockBeendenFragen() { @@ -1207,6 +1294,17 @@ async function lockLoeschen() { closeWarnModal(); + if (_currentLock && _currentLock.controllType === 'TTLOCK') { + document.getElementById('unlockResultBody').innerHTML = ` +
+
🔗
+
Kommuniziere mit TTLock-Server…
+
Bitte warten
+
`; + // Buttons im Modal ausblenden während kommuniziert wird + document.querySelector('#unlockResultModal .warn-modal-actions').style.display = 'none'; + document.getElementById('unlockResultModal').classList.add('open'); + } try { await fetch('/keyholder/timelock/' + lockId, { method: 'DELETE' }); } catch(_) { /* ignorieren */ } @@ -1273,6 +1371,7 @@ closeWarnModal(); closeEmergencyModal(); closeSpinModal(); + document.getElementById('unlockResultModal').classList.remove('open'); } }); diff --git a/xxxthegame/src/main/resources/static/benutzer.html b/xxxthegame/src/main/resources/static/benutzer.html index 5006862..ea27c86 100644 --- a/xxxthegame/src/main/resources/static/benutzer.html +++ b/xxxthegame/src/main/resources/static/benutzer.html @@ -304,6 +304,34 @@ flex-wrap: wrap; } + /* ── Keyholder-Angebote Tab ── */ + .kh-offer-card { + background:var(--color-card); border:1px solid var(--color-secondary); + border-radius:10px; padding:0.75rem 1rem; margin-bottom:0.6rem; + display:flex; align-items:center; gap:0.85rem; + } + .kh-offer-type-icon { + position:relative; width:2.2rem; height:2.2rem; flex-shrink:0; + display:flex; align-items:center; justify-content:center; + } + .kh-offer-type-icon .icon-base { font-size:1.8rem; line-height:1; } + .kh-offer-type-icon img.icon-base { width:1.8rem; height:1.8rem; object-fit:contain; } + .kh-offer-type-icon .icon-lock { + position:absolute; bottom:-2px; right:-4px; + font-size:1.5rem; line-height:1; + filter: drop-shadow(0 0 2px rgba(0,0,0,0.5)); + } + .kh-offer-body { flex:1; min-width:0; } + .kh-offer-name { font-weight:700; font-size:0.95rem; margin-bottom:0.2rem; + white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } + .kh-offer-meta { font-size:0.78rem; color:var(--color-muted); display:flex; flex-wrap:wrap; gap:0.4rem; } + .kh-offer-badge { + display:inline-block; font-size:0.72rem; padding:0.1rem 0.45rem; + border-radius:4px; background:rgba(255,255,255,0.07); border:1px solid var(--color-secondary); + } + .kh-offer-badge.direct { background:rgba(46,204,113,0.12); border-color:rgba(46,204,113,0.3); color:#2ecc71; } + .kh-offer-badge.confirm { background:rgba(230,126,34,0.12); border-color:rgba(230,126,34,0.3); color:#e67e22; } + /* ── Comments (section container) ── */ .comments-section { margin-top: 0.65rem; @@ -406,11 +434,12 @@
- +
+
@@ -435,6 +464,12 @@ + +
+
+ +
+ @@ -682,6 +717,7 @@ // ── Tab switching ── let _gameHistoryLoaded = false; + let _khOffersLoaded = false; function switchProfilTab(name, btn) { document.querySelectorAll('.profil-tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); @@ -691,6 +727,10 @@ _gameHistoryLoaded = true; loadGameHistory(); } + if (name === 'khoffers' && !_khOffersLoaded) { + _khOffersLoaded = true; + loadKhOffers(); + } // URL-QueryParam aktualisieren, damit der Tab nach F5 erhalten bleibt const url = new URL(window.location.href); if (name === 'posts') { @@ -946,6 +986,49 @@ } catch(e) { list.innerHTML = ''; } } + // ── Keyholder-Angebote ── + const KH_GENDER_LABELS = { WEIBLICH: 'Weiblich', MAENNLICH: 'Männlich', DIVERS: 'Divers' }; + + async function loadKhOffers() { + const list = document.getElementById('khOffersList'); + const empty = document.getElementById('khOffersEmpty'); + list.innerHTML = '

Lädt…

'; + try { + const res = await fetch('/keyholder-offers/user/' + targetUserId); + if (!res.ok) { list.innerHTML = ''; return; } + const offers = await res.json(); + list.innerHTML = ''; + if (offers.length === 0) { empty.style.display = ''; return; } + offers.forEach(o => list.appendChild(buildKhOfferCard(o))); + } catch(e) { list.innerHTML = ''; } + } + + function buildKhOfferCard(o) { + const esc = s => { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }; + const genderTags = (o.targetGenders && o.targetGenders.length > 0) + ? o.targetGenders.map(g => `${esc(KH_GENDER_LABELS[g] || g)}`).join('') + : 'Alle'; + const modeBadge = o.directStart + ? 'Direktstart' + : 'Mit Bestätigung'; + const typeIcon = o.templateType === 'TIMELOCK' + ? `
🕐🔒
` + : `
Karten-Lock🔒
`; + + const div = document.createElement('div'); + div.className = 'kh-offer-card'; + div.innerHTML = ` + ${typeIcon} +
+
${esc(o.templateName || 'Unbenannt')}
+
+ ${modeBadge} ${genderTags} + ✓ ${o.acceptanceCount}× angenommen +
+
`; + return div; + } + async function postPinnwand() { const ta = document.getElementById('pinnwandText'); const text = ta.value.trim(); diff --git a/xxxthegame/src/main/resources/static/einstellungen.html b/xxxthegame/src/main/resources/static/einstellungen.html index 1f7ef08..88c146d 100644 --- a/xxxthegame/src/main/resources/static/einstellungen.html +++ b/xxxthegame/src/main/resources/static/einstellungen.html @@ -566,6 +566,18 @@ + +
+
+
Profil bei Veröffentlichungen sichtbar
+
Dein Name wird bei veröffentlichten Lock-Vorlagen angezeigt
+
+ +
+
@@ -955,13 +967,14 @@ 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'); + 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'); + setValue('sv-veroeffentlichungen', profile.profilBeiVeroeffentlichungenSichtbar ? 'true' : 'false'); } function setValue(id, value) { @@ -971,13 +984,14 @@ 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, + 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, + profilBeiVeroeffentlichungenSichtbar: document.getElementById('sv-veroeffentlichungen').value === 'true', }; const res = await fetch('/user/me/privacy', { method: 'PUT', diff --git a/xxxthegame/src/main/resources/static/entdecken-vorlagen.html b/xxxthegame/src/main/resources/static/entdecken-vorlagen.html new file mode 100644 index 0000000..854f3f1 --- /dev/null +++ b/xxxthegame/src/main/resources/static/entdecken-vorlagen.html @@ -0,0 +1,528 @@ + + + + + + + Vorlagen entdecken – xXx Sphere + + + + + +
+
+ +

🔍 Vorlagen entdecken

+ + + + + +
+
+ + + +
+
+ + +
+
+
+
+ +
+

+
+
+
+ +
+ +
+ + +
+
+ + + + + + + diff --git a/xxxthegame/src/main/resources/static/js/card-defs.js b/xxxthegame/src/main/resources/static/js/card-defs.js index 65f3579..765fc0f 100644 --- a/xxxthegame/src/main/resources/static/js/card-defs.js +++ b/xxxthegame/src/main/resources/static/js/card-defs.js @@ -66,7 +66,7 @@ const CARD_DEFS = [ id: 'CUM', img: '/img/card_cum.png', name: 'Cum', - desc: 'Spezielle Karte.', + desc: 'Du wirst entsperrt, nutze diese Entsperrung um zu kommen. Je länger du brauchst, desto schlimmer.', defMin: 0, defMax: 0, }, @@ -74,7 +74,7 @@ const CARD_DEFS = [ id: 'CUM_IN_CAGE', img: '/img/card_cum_caged.png', name: 'Cum in Cage', - desc: 'Spezielle Karte.', + desc: 'Komme in deinem Keuschheitsgürtel, wie du es anstellst ist deine Sache.', defMin: 0, defMax: 0, }, diff --git a/xxxthegame/src/main/resources/static/js/sidebar.js b/xxxthegame/src/main/resources/static/js/sidebar.js index 90ff49d..e6da6e5 100644 --- a/xxxthegame/src/main/resources/static/js/sidebar.js +++ b/xxxthegame/src/main/resources/static/js/sidebar.js @@ -29,7 +29,9 @@ { href: '/neulock.html', icon: I('NEW_LOCK'), label: 'Neues Lock', id: 'navChastityNeu' }, { href: '#', icon: I('ACTIVE_LOCK'), label: 'Aktives Lock', id: 'navChastityAktiv' }, { href: '/communityvotes.html', icon: I('VOTES'), label: 'Community Votes' }, - { href: '/meine-locks.html', icon: I('LOCK'), label: 'Meine Locks' }, + { href: '/meine-locks.html', icon: I('LOCK'), label: 'Meine Vorlagen' }, + { href: '/entdecken-vorlagen.html', icon: I('DISCOVER'), label: 'Entdecken' }, + { href: '/keyholder-finden.html', icon: I('FRIENDS'), label: 'Keyholder finden' }, { href: '/keyholder.html', icon: I('KEY'), label: 'Keyholder' }, { href: '/unlock-history.html', icon: I('HISTORY'), label: 'Code-Historie' }, ] diff --git a/xxxthegame/src/main/resources/static/js/topbar.js b/xxxthegame/src/main/resources/static/js/topbar.js index 9e6bce5..4e66c3b 100644 --- a/xxxthegame/src/main/resources/static/js/topbar.js +++ b/xxxthegame/src/main/resources/static/js/topbar.js @@ -277,24 +277,25 @@ if (!res.ok) { body.innerHTML = '
Keine Benachrichtigungen.
'; return; } const notifs = await res.json(); if (!notifs.length) { body.innerHTML = '
Keine neuen Benachrichtigungen.
'; return; } - body.innerHTML = `
- -
`; + body.innerHTML = ''; notifs.forEach(n => { const el = document.createElement('div'); - const tag = n.targetUrl ? 'a' : 'div'; - const href = n.targetUrl ? `href="${esc(n.targetUrl)}"` : ''; - const unread = !n.read; - el.innerHTML = `<${tag} ${href} class="topbar-panel-item topbar-notif-item${unread ? ' topbar-notif-item--unread' : ''}" - onclick="window.__topbarMarkNotifRead('${esc(n.id)}')"> - ${unread ? '' : ''} + const tag = n.targetUrl ? 'a' : 'div'; + const href = n.targetUrl ? `href="${esc(n.targetUrl)}"` : ''; + const av = n.senderAvatar + ? `` + : `${IC('PROFILE')}`; + el.innerHTML = `<${tag} ${href} class="topbar-panel-item topbar-notif-item"> + ${av}
-
${esc(n.text)}
+
${esc(n.text)}
${n.sentAt ? new Date(n.sentAt).toLocaleString('de-DE',{dateStyle:'short',timeStyle:'short'}) : ''}
`; body.appendChild(el.firstElementChild); }); + // Alle als gelesen markieren + fetch('/notifications/read-all', { method: 'POST' }).then(() => setTopbarBadge('notif', 0)).catch(() => {}); } catch (e) { body.innerHTML = '
Fehler beim Laden.
'; } } diff --git a/xxxthegame/src/main/resources/static/keyholder-finden.html b/xxxthegame/src/main/resources/static/keyholder-finden.html new file mode 100644 index 0000000..515a2b8 --- /dev/null +++ b/xxxthegame/src/main/resources/static/keyholder-finden.html @@ -0,0 +1,530 @@ + + + + + + + Keyholder finden – xXx Sphere + + + + + +
+
+

🔍 Keyholder finden

+

+ Hier findest du Nutzer*innen, die sich als Keyholder für ein bestimmtes Lock-Template anbieten. + Die beliebtesten Angebote erscheinen ganz oben. +

+ +
+ +

Wird geladen…

+
+
+ + +
+
+ +
+ +
+

+
+
+
+
+ +
+
+ + +
+
+ +

🔒 Angebot annehmen

+

+ +
+
Schloss-Steuerung
+ +
+ +
+
Code-Länge
+ +
+ + + +
+ + +
+
+
+ + +
+
+
🔒
+

+

+ +
+ + +
+
+
+ + + + + + + diff --git a/xxxthegame/src/main/resources/static/keyholder.html b/xxxthegame/src/main/resources/static/keyholder.html index 4dffb67..be5d197 100644 --- a/xxxthegame/src/main/resources/static/keyholder.html +++ b/xxxthegame/src/main/resources/static/keyholder.html @@ -101,16 +101,154 @@ .tp-seg .tp-label { font-size:0.65rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; } .tp-colon { font-size:1.1rem; font-weight:700; color:var(--color-muted); margin-bottom:1rem; } + /* ── Tab-Navigation ── */ + .kh-tabs { display:flex; gap:0; border-bottom:2px solid var(--color-secondary); margin-bottom:1.25rem; } + .kh-tab { + padding:0.55rem 1.1rem; font-size:0.9rem; font-weight:600; cursor:pointer; + border:none; background:none; color:var(--color-muted); + border-bottom:2px solid transparent; margin-bottom:-2px; + transition:color 0.15s, border-color 0.15s; + } + .kh-tab.active { color:var(--color-text); border-bottom-color:var(--color-primary); } + + /* ── Vorlagen-Typ-Icon (wie in meine-locks.html) ── */ + .template-type-icon { + position:relative; width:2.2rem; height:2.2rem; flex-shrink:0; + display:flex; align-items:center; justify-content:center; + } + .template-type-icon .icon-base { font-size:1.8rem; line-height:1; } + .template-type-icon img.icon-base { width:1.8rem; height:1.8rem; object-fit:contain; } + .template-type-icon .icon-lock { + position:absolute; bottom:-2px; right:-4px; + font-size:1.5rem; line-height:1; + filter: drop-shadow(0 0 2px rgba(0,0,0,0.5)); + } + + /* ── Angebote-Tab ── */ + .offer-card { + background:var(--color-card); border:1px solid var(--color-secondary); + border-radius:10px; padding:0.85rem 1rem; margin-bottom:0.6rem; + display:flex; align-items:center; gap:0.85rem; + } + .offer-card-body { flex:1; min-width:0; } + .offer-card-name { font-weight:700; font-size:0.95rem; margin-bottom:0.2rem; + white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } + .offer-card-meta { font-size:0.78rem; color:var(--color-muted); display:flex; flex-wrap:wrap; gap:0.4rem; } + .offer-badge { + display:inline-block; font-size:0.72rem; padding:0.1rem 0.45rem; + border-radius:4px; background:rgba(255,255,255,0.07); border:1px solid var(--color-secondary); + } + .offer-badge.direct { background:rgba(46,204,113,0.12); border-color:rgba(46,204,113,0.3); color:#2ecc71; } + .offer-badge.confirm { background:rgba(230,126,34,0.12); border-color:rgba(230,126,34,0.3); color:#e67e22; } + .btn-offer-del { + background:rgba(231,76,60,0.1); border:1px solid rgba(231,76,60,0.3); color:#e74c3c; + border-radius:7px; padding:0.3rem 0.65rem; font-size:0.8rem; cursor:pointer; flex-shrink:0; width:auto; + } + + /* Combobox (Vorlage-Auswahl im Angebot-Modal) */ + .combo-wrap { position: relative; } + .combo-wrap input[type="text"] { width: 100%; box-sizing: border-box; } + .combo-dropdown { + display: none; position: absolute; top: calc(100% + 3px); + 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: 600; + box-shadow: 0 4px 16px rgba(0,0,0,0.25); + } + .combo-dropdown.open { display: block; } + .combo-option { + padding: 0.55rem 0.85rem; cursor: pointer; + font-size: 0.9rem; color: var(--color-text); + } + .combo-option:hover { background: var(--color-secondary); } + .combo-empty { padding: 0.55rem 0.85rem; font-size: 0.85rem; color: var(--color-muted); font-style: italic; }
-

Keyholder

+

Keyholder

- -
- + +
+ + +
+ + +
+
+ +
+ + + +
+
+ + + @@ -409,6 +547,17 @@ body.dataset.loaded = '1'; attachDetailListeners(body, lockId); } + // Listenkarte line3 aktualisieren + const line3 = card ? card.querySelector('.lock-card-line3') : null; + if (line3) { + const startDate = d.startTime ? new Date(d.startTime).toLocaleDateString('de-DE') : '–'; + const frozenBadge = (d.isFrozenByKeyholder || d.isFrozen) ? ' · ❄️ Eingefroren' : ''; + if (d.lockType === 'TIMELOCK') { + line3.textContent = `⏱ TimeLock · seit ${startDate}${frozenBadge}`; + } else { + line3.textContent = `🃏 ${d.totalCards} Karten · seit ${startDate}${frozenBadge}`; + } + } } catch(e) { if (body) body.textContent = 'Fehler beim Laden.'; } } @@ -463,6 +612,24 @@ html += ``; } + // Einfrieren + html += `
Einfrieren
`; + if (d.isFrozen) { + const fu = d.frozenUntil ? new Date(d.frozenUntil).toLocaleString('de-DE') : 'unbegrenzt'; + html += `
+ ❄️ Eingefroren bis + ${fu} +
+
+ +
`; + } else { + html += `
+ +
`; + } + html += `
`; + // Gestartet am if (d.startTime) { html += `
@@ -804,18 +971,7 @@ }); if (res.ok || res.status === 202) { closeVerificationModal(); - const cardEl = document.querySelector(`[data-lock-id="${lockId}"]`); - if (cardEl) { - const body = cardEl.querySelector('.lock-detail-body'); - const detailRes = await fetch('/keyholder/as-keyholder/' + lockId); - if (detailRes.ok) { - const updated = await detailRes.json(); - lockDetailCache[lockId] = updated; - body.innerHTML = buildDetailHtml(updated); - body.dataset.loaded = '1'; - attachDetailListeners(body, lockId); - } - } + await reloadLockDetail(lockId); } else { btnUp.disabled = btnDown.disabled = false; } @@ -903,19 +1059,7 @@ }); if (res.ok || res.status === 204) { closeCardModal(); - // Detail neu laden - const cardEl = document.querySelector(`[data-lock-id="${lockId}"]`); - if (cardEl) { - const body = cardEl.querySelector('.lock-detail-body'); - const detailRes = await fetch('/keyholder/as-keyholder/' + lockId); - if (detailRes.ok) { - const d = await detailRes.json(); - lockDetailCache[lockId] = d; - body.innerHTML = buildDetailHtml(d); - body.dataset.loaded = '1'; - attachDetailListeners(body, lockId); - } - } + await reloadLockDetail(lockId); } else { const data = await res.json().catch(() => ({})); const errEl = document.getElementById('cardEditError'); @@ -1270,8 +1414,11 @@ } errEl.style.display = 'none'; const frozenUntil = new Date(Date.now() + minutes * 60000).toISOString().slice(0, 19); + const freezeEndpoint = lockTypeMap[lockId] === 'TIMELOCK' + ? `/keyholder/timelock/as-keyholder/${lockId}/freeze` + : `/keyholder/as-keyholder/${lockId}/freeze`; try { - const res = await fetch(`/keyholder/as-keyholder/${lockId}/freeze`, { + const res = await fetch(freezeEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ frozenUntil }) @@ -1302,8 +1449,11 @@ async function submitUnfreeze() { const lockId = unfreezeTargetLockId; + const unfreezeEndpoint = lockTypeMap[lockId] === 'TIMELOCK' + ? `/keyholder/timelock/as-keyholder/${lockId}/freeze` + : `/keyholder/as-keyholder/${lockId}/freeze`; try { - const res = await fetch(`/keyholder/as-keyholder/${lockId}/freeze`, { method: 'DELETE' }); + const res = await fetch(unfreezeEndpoint, { method: 'DELETE' }); if (res.ok || res.status === 204) { closeUnfreezeModal(); await reloadLockDetail(lockId); @@ -1311,26 +1461,6 @@ } catch(e) { console.error(e); } } - async function reloadLockDetail(lockId) { - const cardEl = document.querySelector(`[data-lock-id="${lockId}"]`); - if (!cardEl) return; - const body = cardEl.querySelector('.lock-detail-body'); - const detailRes = await fetch('/keyholder/as-keyholder/' + lockId); - if (detailRes.ok) { - const updated = await detailRes.json(); - lockDetailCache[lockId] = updated; - body.innerHTML = buildDetailHtml(updated); - body.dataset.loaded = '1'; - attachDetailListeners(body, lockId); - // Listenkarte line3 aktualisieren - const line3 = cardEl.querySelector('.lock-card-line3'); - if (line3) { - const startDate = updated.startTime ? new Date(updated.startTime).toLocaleDateString('de-DE') : '–'; - const frozenBadge = updated.isFrozenByKeyholder ? ' · ❄️ Eingefroren' : ''; - line3.textContent = `🃏 ${updated.totalCards} Karten · seit ${startDate}${frozenBadge}`; - } - } - } async function chooseTaskForLock(choiceId, taskIndex, lockId) { try { @@ -1454,8 +1584,9 @@ } catch(e) { console.error(e); } } - // Initial laden, dann ggf. per URL-Parameter ein Lock vorauswählen + // Initial laden, dann ggf. per URL-Parameter ein Lock vorauswählen / Tab wiederherstellen loadLocks().then(() => { + restoreTabFromUrl(); const params = new URLSearchParams(window.location.search); const preselect = params.get('lockId'); if (preselect) { @@ -1474,6 +1605,247 @@ if (lockId) reloadLockDetail(lockId); }); }, 60000); + + // ── Tab-Switching ────────────────────────────────────────────────────────── + function switchTab(tab) { + document.querySelectorAll('.kh-tab').forEach(b => b.classList.remove('active')); + event.currentTarget.classList.add('active'); + document.getElementById('tabLockees').style.display = tab === 'lockees' ? '' : 'none'; + document.getElementById('tabOffers').style.display = tab === 'offers' ? '' : 'none'; + if (tab === 'offers') loadOffers(); + + const url = new URL(window.location.href); + if (tab === 'lockees') url.searchParams.delete('tab'); + else url.searchParams.set('tab', tab); + history.replaceState(null, '', url.toString()); + } + + function restoreTabFromUrl() { + const tab = new URLSearchParams(window.location.search).get('tab'); + if (tab === 'offers') { + const btn = document.querySelector('.kh-tab:nth-child(2)'); + if (btn) { btn.click(); } + } + } + + // ── Keyholder-Angebote ──────────────────────────────────────────────────── + let _offerTemplates = []; + let _editOfferId = null; + + function esc2(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; } + + const GENDER_LABELS = { WEIBLICH: 'Weiblich', MAENNLICH: 'Männlich', DIVERS: 'Divers' }; + + async function loadOffers() { + const res = await fetch('/keyholder-offers/mine'); + if (!res.ok) return; + const offers = await res.json(); + const list = document.getElementById('offersList'); + const empty = document.getElementById('offersEmpty'); + list.innerHTML = ''; + if (offers.length === 0) { empty.style.display = ''; return; } + empty.style.display = 'none'; + offers.forEach(o => list.appendChild(buildOfferCard(o))); + } + + function buildOfferCard(o) { + const genderTags = (o.targetGenders && o.targetGenders.length > 0) + ? o.targetGenders.map(g => `${esc2(GENDER_LABELS[g] || g)}`).join('') + : 'Alle'; + const modeBadge = o.directStart + ? 'Direktstart' + : 'Mit Bestätigung'; + const typeIcon = o.templateType === 'TIMELOCK' + ? `
🕐🔒
` + : `
Karten-Lock🔒
`; + const div = document.createElement('div'); + div.className = 'offer-card'; + div.style.cursor = 'pointer'; + div.innerHTML = ` + ${typeIcon} +
+
${esc2(o.templateName)}
+
+ ${modeBadge} ${genderTags} + ✓ ${o.acceptanceCount}× angenommen +
+
+ `; + div.addEventListener('click', () => openEditOfferModal(o)); + return div; + } + + async function deleteOffer(id) { + if (!confirm('Angebot wirklich entfernen?')) return; + const res = await fetch(`/keyholder-offers/${id}`, { method: 'DELETE' }); + if (res.ok || res.status === 204) loadOffers(); + else alert('Fehler beim Entfernen.'); + } + + // ── Angebot erstellen / bearbeiten ──────────────────────────────────────── + async function _openOfferModal() { + document.getElementById('createOfferError').style.display = 'none'; + document.getElementById('createOfferSubmitBtn').disabled = false; + + // Templates laden (eigene + abonnierte) + const [ownRes, subRes] = await Promise.all([ + fetch('/templates/mine'), + fetch('/templates/subscribed') + ]); + const own = ownRes.ok ? await ownRes.json() : []; + const sub = subRes.ok ? await subRes.json() : []; + const ownIds = new Set(own.map(t => t.templateId)); + _offerTemplates = [...own, ...sub.filter(t => !ownIds.has(t.templateId))]; + setupOfferTemplateCombo(); + } + + async function openCreateOfferModal() { + _editOfferId = null; + document.getElementById('offerModalTitle').textContent = '🔑 Keyholder-Angebot erstellen'; + document.getElementById('createOfferSubmitBtn').textContent = 'Erstellen'; + + await _openOfferModal(); + + document.getElementById('offerTemplateInput').value = ''; + document.getElementById('offerTemplateValue').value = ''; + document.getElementById('offerGenderAll').checked = false; + document.querySelectorAll('input[name="offerGender"]').forEach(cb => { cb.checked = false; cb.disabled = false; }); + document.querySelector('input[name="offerStartMode"][value="direct"]').checked = true; + document.getElementById('createOfferModal').style.display = 'flex'; + } + + async function openEditOfferModal(o) { + _editOfferId = o.id; + document.getElementById('offerModalTitle').textContent = '✏️ Keyholder-Angebot bearbeiten'; + document.getElementById('createOfferSubmitBtn').textContent = 'Speichern'; + + await _openOfferModal(); + + // Vorlage vorauswählen + const tpl = _offerTemplates.find(t => t.templateId === o.templateId); + if (tpl) { + const badge = tpl.lockType === 'TIMELOCK' ? '⏱' : '🃏'; + document.getElementById('offerTemplateInput').value = badge + ' ' + (tpl.name || 'Unbenannte Vorlage'); + document.getElementById('offerTemplateValue').value = tpl.templateId; + } else { + // Template nicht mehr in eigenen/abonnierten → Fallback: Name anzeigen + document.getElementById('offerTemplateInput').value = o.templateName || ''; + document.getElementById('offerTemplateValue').value = o.templateId; + } + + // Geschlechter vorauswählen + const genders = o.targetGenders || []; + const allSelected = genders.length === 0; + document.getElementById('offerGenderAll').checked = allSelected; + document.querySelectorAll('input[name="offerGender"]').forEach(cb => { + cb.checked = !allSelected && genders.includes(cb.value); + cb.disabled = allSelected; + }); + + // Startmodus + const modeVal = o.directStart ? 'direct' : 'confirm'; + document.querySelector(`input[name="offerStartMode"][value="${modeVal}"]`).checked = true; + + document.getElementById('createOfferModal').style.display = 'flex'; + } + + function setupOfferTemplateCombo() { + const input = document.getElementById('offerTemplateInput'); + const dropdown = document.getElementById('offerTemplateDropdown'); + const hidden = document.getElementById('offerTemplateValue'); + + // Vorherige Listener entfernen durch Klonen + const newInput = input.cloneNode(true); + input.parentNode.replaceChild(newInput, input); + + function renderDropdown(query) { + const q = query.toLowerCase().trim(); + const filtered = q + ? _offerTemplates.filter(t => (t.name || '').toLowerCase().includes(q)) + : _offerTemplates; + dropdown.innerHTML = ''; + if (filtered.length === 0) { + dropdown.innerHTML = '
Keine Vorlagen gefunden.
'; + } else { + filtered.forEach(t => { + const badge = t.lockType === 'TIMELOCK' ? '⏱' : '🃏'; + const label = t.name || 'Unbenannte Vorlage'; + const div = document.createElement('div'); + div.className = 'combo-option'; + div.innerHTML = `${badge} ${label}`; + div.addEventListener('mousedown', e => { + e.preventDefault(); + hidden.value = t.templateId; + newInput.value = badge + ' ' + label; + dropdown.classList.remove('open'); + }); + dropdown.appendChild(div); + }); + } + dropdown.classList.add('open'); + } + + newInput.addEventListener('input', () => { hidden.value = ''; renderDropdown(newInput.value); }); + newInput.addEventListener('focus', () => renderDropdown(newInput.value)); + newInput.addEventListener('blur', () => { + setTimeout(() => { + dropdown.classList.remove('open'); + if (!hidden.value) newInput.value = ''; + }, 150); + }); + } + + function closeCreateOfferModal() { + document.getElementById('createOfferModal').style.display = 'none'; + _editOfferId = null; + } + + function toggleAllGenders(allCb) { + document.querySelectorAll('input[name="offerGender"]').forEach(cb => { cb.checked = false; cb.disabled = allCb.checked; }); + } + + async function submitCreateOffer() { + const templateId = document.getElementById('offerTemplateValue').value; + if (!templateId) { showOfferError('Bitte eine Vorlage auswählen.'); return; } + + const allGenders = document.getElementById('offerGenderAll').checked; + const targetGenders = allGenders + ? [] + : Array.from(document.querySelectorAll('input[name="offerGender"]:checked')).map(cb => cb.value); + + const directStart = document.querySelector('input[name="offerStartMode"]:checked').value === 'direct'; + + const btn = document.getElementById('createOfferSubmitBtn'); + btn.disabled = true; + + const url = _editOfferId ? `/keyholder-offers/${_editOfferId}` : '/keyholder-offers'; + const method = _editOfferId ? 'PATCH' : 'POST'; + + const res = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ templateId, targetGenders, directStart }) + }); + if (res.ok) { + closeCreateOfferModal(); + loadOffers(); + } else if (res.status === 403) { + const d = await res.json().catch(() => ({})); + showOfferError(d.error === 'offer_limit_reached' + ? 'Limit erreicht – upgrade auf Premium für unbegrenzte Angebote.' + : 'Keine Berechtigung.'); + btn.disabled = false; + } else { + showOfferError(_editOfferId ? 'Fehler beim Speichern.' : 'Fehler beim Erstellen.'); + btn.disabled = false; + } + } + + function showOfferError(msg) { + const el = document.getElementById('createOfferError'); + el.textContent = msg; + el.style.display = ''; + } diff --git a/xxxthegame/src/main/resources/static/meine-locks.html b/xxxthegame/src/main/resources/static/meine-locks.html index 31aa780..137d536 100644 --- a/xxxthegame/src/main/resources/static/meine-locks.html +++ b/xxxthegame/src/main/resources/static/meine-locks.html @@ -4,7 +4,7 @@ - Meine Locks – xXx Sphere + Meine Vorlagen – xXx Sphere