Neues Feature für Vorlieben hinzugeüft, Bugfixes im Vanilla game
@@ -72,7 +72,8 @@
|
||||
"Bash(mv bdsm-einladung.html games/bdsm/)",
|
||||
"Bash(mv bdsmingame.html games/bdsm/)",
|
||||
"Bash(mv bdsmplayers.html games/bdsm/)",
|
||||
"Bash(perl -pi -e 's|\\\\.requestMatchers\\\\\\(\"\"/\\\\*\\\\.html\"\"\\\\\\)\\\\.permitAll\\\\\\(\\\\\\)|.requestMatchers\\(\"\"/*.html\"\"\\).permitAll\\(\\)\\\\n .requestMatchers\\(\"\"/**/*.html\"\"\\).permitAll\\(\\)|' /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java)"
|
||||
"Bash(perl -pi -e 's|\\\\.requestMatchers\\\\\\(\"\"/\\\\*\\\\.html\"\"\\\\\\)\\\\.permitAll\\\\\\(\\\\\\)|.requestMatchers\\(\"\"/*.html\"\"\\).permitAll\\(\\)\\\\n .requestMatchers\\(\"\"/**/*.html\"\"\\).permitAll\\(\\)|' /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/config/SecurityConfig.java)",
|
||||
"Bash(./gradlew compileJava -x test)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#Mon Mar 30 07:33:01 CEST 2026
|
||||
#Tue Mar 31 20:02:52 CEST 2026
|
||||
display=\:0
|
||||
host=mario-mint
|
||||
process-id=2955
|
||||
process-id=9888
|
||||
user=mario
|
||||
|
||||
174
.metadata/.log
@@ -1379,3 +1379,177 @@ Binding(CTRL+SHIFT+T,
|
||||
,,true),null),
|
||||
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||
org.eclipse.ui.contexts.window,,,system)
|
||||
|
||||
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-30 22:56:58.269
|
||||
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
|
||||
!SESSION 2026-03-31 08:24:30.017 -----------------------------------------------
|
||||
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-31 08:24:31.551
|
||||
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
|
||||
|
||||
!ENTRY ch.qos.logback.classic 1 0 2026-03-31 08:24:34.912
|
||||
!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-31 08:24:35.071
|
||||
!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-31 08:24:35.071
|
||||
!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-31 08:24:35.221
|
||||
!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-31 08:24:35.221
|
||||
!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-31 09:50:47.663
|
||||
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-31 09:50:47.663
|
||||
!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-31 10:19:59.888
|
||||
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
|
||||
!SESSION 2026-03-31 11:33:04.112 -----------------------------------------------
|
||||
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-31 11:33:05.636
|
||||
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
|
||||
|
||||
!ENTRY ch.qos.logback.classic 1 0 2026-03-31 11:33:15.062
|
||||
!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-31 11:33:15.221
|
||||
!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-31 11:33:15.221
|
||||
!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-31 11:33:15.366
|
||||
!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-31 11:33:15.366
|
||||
!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-31 11:35:22.321
|
||||
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-31 11:35:22.321
|
||||
!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-31 15:25:37.605
|
||||
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
|
||||
!SESSION 2026-03-31 20:02:49.357 -----------------------------------------------
|
||||
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-31 20:02:50.906
|
||||
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
|
||||
|
||||
!ENTRY ch.qos.logback.classic 1 0 2026-03-31 20:02:53.313
|
||||
!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-31 20:02:53.439
|
||||
!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-31 20:02:53.440
|
||||
!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-31 20:02:53.586
|
||||
!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-31 20:02:53.586
|
||||
!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-31 20:48:38.922
|
||||
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-31 20:48:38.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.lsp4e 2 0 2026-03-31 21:38:55.972
|
||||
!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-31 22:11:43.432
|
||||
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-31 22:11:43.432
|
||||
!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)
|
||||
|
||||
@@ -1,24 +1,7 @@
|
||||
[ {
|
||||
"version" : "9.6.0-20260329003549+0000",
|
||||
"buildTime" : "20260329003549+0000",
|
||||
"commitId" : "db62c2f2b404217cb6a7eef2598c6e84ab08fa27",
|
||||
"current" : false,
|
||||
"snapshot" : true,
|
||||
"nightly" : true,
|
||||
"releaseNightly" : false,
|
||||
"activeRc" : false,
|
||||
"rcFor" : "",
|
||||
"milestoneFor" : "",
|
||||
"broken" : false,
|
||||
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260329003549+0000-bin.zip",
|
||||
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260329003549+0000-bin.zip.sha256",
|
||||
"checksum" : "27b9c08aeaf720b9ee44dc6eef5543699bafba27772aa8a33e64cf964a2ea958",
|
||||
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260329003549+0000-wrapper.jar.sha256",
|
||||
"wrapperChecksum" : "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7"
|
||||
}, {
|
||||
"version" : "9.5.0-20260328024422+0000",
|
||||
"buildTime" : "20260328024422+0000",
|
||||
"commitId" : "a90300e5c547f6d0416d765f1ef285d1ecb589f9",
|
||||
"version" : "9.5.0-20260331054436+0000",
|
||||
"buildTime" : "20260331054436+0000",
|
||||
"commitId" : "04cdc7917382feb3229f13b035ea48f106ad01f1",
|
||||
"current" : false,
|
||||
"snapshot" : true,
|
||||
"nightly" : false,
|
||||
@@ -27,10 +10,44 @@
|
||||
"rcFor" : "",
|
||||
"milestoneFor" : "",
|
||||
"broken" : false,
|
||||
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260328024422+0000-bin.zip",
|
||||
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260328024422+0000-bin.zip.sha256",
|
||||
"checksum" : "f921ed9b701b2046ba53c3c499df12ebab7b70b6d58c83337d9375427f9af2ee",
|
||||
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260328024422+0000-wrapper.jar.sha256",
|
||||
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260331054436+0000-bin.zip",
|
||||
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260331054436+0000-bin.zip.sha256",
|
||||
"checksum" : "4ab20ff318524006769da0e39fa7cf8f355a5ca54ea213dd2edadd9019d95649",
|
||||
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260331054436+0000-wrapper.jar.sha256",
|
||||
"wrapperChecksum" : "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7"
|
||||
}, {
|
||||
"version" : "9.6.0-20260331012943+0000",
|
||||
"buildTime" : "20260331012943+0000",
|
||||
"commitId" : "6921c9df28f41760c3a348e57a9bf332d093742e",
|
||||
"current" : false,
|
||||
"snapshot" : true,
|
||||
"nightly" : true,
|
||||
"releaseNightly" : false,
|
||||
"activeRc" : false,
|
||||
"rcFor" : "",
|
||||
"milestoneFor" : "",
|
||||
"broken" : false,
|
||||
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260331012943+0000-bin.zip",
|
||||
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260331012943+0000-bin.zip.sha256",
|
||||
"checksum" : "83f35f9ee38851b1835842ba93b9846fc43be77792c2cae138570c5f262039e0",
|
||||
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260331012943+0000-wrapper.jar.sha256",
|
||||
"wrapperChecksum" : "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7"
|
||||
}, {
|
||||
"version" : "9.5.0-rc-1",
|
||||
"buildTime" : "20260330120715+0000",
|
||||
"commitId" : "6a1704c113de068f7e9a6744245c7eb4bc5091d0",
|
||||
"current" : false,
|
||||
"snapshot" : false,
|
||||
"nightly" : false,
|
||||
"releaseNightly" : false,
|
||||
"activeRc" : true,
|
||||
"rcFor" : "9.5.0",
|
||||
"milestoneFor" : "",
|
||||
"broken" : false,
|
||||
"downloadUrl" : "https://services.gradle.org/distributions/gradle-9.5.0-rc-1-bin.zip",
|
||||
"checksumUrl" : "https://services.gradle.org/distributions/gradle-9.5.0-rc-1-bin.zip.sha256",
|
||||
"checksum" : "66d79b10eb939c954bf1ac3be9d9cde985301b56058d49542286c35782ae1e74",
|
||||
"wrapperChecksumUrl" : "https://services.gradle.org/distributions/gradle-9.5.0-rc-1-wrapper.jar.sha256",
|
||||
"wrapperChecksum" : "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7"
|
||||
}, {
|
||||
"version" : "9.4.1",
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.cardlock{CardLockEntity.java[CardLockEntity" modifiers="1" timestamp="1774814915718"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.user{UserController.java[UserController" modifiers="1" timestamp="1774814915722"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.ttlock{TTLockService.java[TTLockService" modifiers="1" timestamp="1774814915716"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.timelock{TimeLockController.java[TimeLockController" modifiers="1" timestamp="1774814915715"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.timelock{TimeLockController.java[TimeLockController" modifiers="1" timestamp="1774900567883"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.common{BaseLockService.java[BaseLockService" modifiers="1025" timestamp="1774814915716"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.ttlock{TTLockCallback.java[TTLockCallback" modifiers="1" timestamp="1774814915716"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.lockcontroll{TTLockControl.java[TTLockControl" modifiers="1" timestamp="1774814915717"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.cardlock{CardLockController.java[CardLockController" modifiers="1" timestamp="1774814915718"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.cardlock{CardLockController.java[CardLockController" modifiers="1" timestamp="1774903113531"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.common{BaseLockEntity.java[BaseLockEntity" modifiers="1" timestamp="1774814915716"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.cardlock{CardLockService.java[CardLockService" modifiers="1" timestamp="1774814915718"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.timelock{TimeLockService.java[TimeLockService" modifiers="1" timestamp="1774814915716"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.common.aufgaben{Aufgabe.java[Aufgabe" modifiers="1" timestamp="1774814915721"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.common.aufgaben{DefaultFiller.java[DefaultFiller" modifiers="1" timestamp="1774814915721"/>
|
||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.mail{MailService.java[MailService" modifiers="1" timestamp="1774814915713"/>
|
||||
</typeInfoHistroy>
|
||||
|
||||
@@ -34,4 +34,5 @@
|
||||
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.unlock.TempOpeningReason"/>
|
||||
<fullyQualifiedTypeName name="de.oaa.xxx.games.vanilla.VanillaMitspieler"/>
|
||||
<fullyQualifiedTypeName name="de.oaa.xxx.games.common.aufgaben.CommonMitspieler"/>
|
||||
<fullyQualifiedTypeName name="java.util.Comparator"/>
|
||||
</qualifiedTypeNameHistroy>
|
||||
|
||||
@@ -16,3 +16,6 @@
|
||||
2026-03-27 07:46:24,300 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
||||
2026-03-29 16:28:13,219 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
|
||||
2026-03-30 07:33:05,316 [Worker-5: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
||||
2026-03-31 08:24:38,073 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
|
||||
2026-03-31 11:33:17,509 [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-31 20:02:56,538 [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.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#Mon Mar 30 07:33:01 CEST 2026
|
||||
#Tue Mar 31 20:02:52 CEST 2026
|
||||
org.eclipse.core.runtime=2
|
||||
org.eclipse.platform=4.39.0.v20260226-0420
|
||||
|
||||
BIN
bilder/Gemini_Generated_Image_xdceypxdceypxdce.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
bilder/dunno.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
bilder/negative.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
bilder/neutral.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
bilder/positiv.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
bilder/verynegative.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
bilder/verypositiv.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
21
xxxthegame/deploy.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Konfiguration
|
||||
REMOTE_CONTEXT="proxmox-remote"
|
||||
IMAGE_NAME="xxx-sphere"
|
||||
TAG="latest"
|
||||
|
||||
echo "--- 1. Gradle Build: Erstelle Docker Image lokal ---"
|
||||
# Dieser Befehl baut die Jar UND das Docker Image direkt in deinem lokalen Docker
|
||||
./gradlew bootBuildImage --imageName=$IMAGE_NAME:$TAG
|
||||
|
||||
echo "--- 2. Transfer: Image zum Proxmox-Server schieben ---"
|
||||
# Wir 'pipen' das Image direkt über SSH auf den Zielserver
|
||||
docker save $IMAGE_NAME:$TAG | docker --context $REMOTE_CONTEXT load
|
||||
|
||||
echo "--- 3. Remote Deployment: Starten auf Proxmox ---"
|
||||
# Wir führen Docker Compose direkt im Remote-Kontext aus
|
||||
# --force-recreate stellt sicher, dass die App mit dem neuen Image neu startet
|
||||
docker --context $REMOTE_CONTEXT compose up -d --force-recreate
|
||||
|
||||
echo "--- Fertig! Die App läuft auf dem Proxmox-Server ---"
|
||||
32
xxxthegame/docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
db:
|
||||
image: mysql:8.0
|
||||
container_name: mysql-db
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_DATABASE: xxx_sphere
|
||||
MYSQL_ROOT_PASSWORD: xxxsphere123!
|
||||
ports:
|
||||
- "3306:3306" # <--- Jetzt steht es korrekt alleine!
|
||||
volumes:
|
||||
# Format: [Pfad auf dem Proxmox-Host]:[Pfad im Container]
|
||||
- /mnt/pve_nas/.mysql_data:/var/lib/mysql
|
||||
|
||||
app:
|
||||
image: xxx-sphere:latest
|
||||
container_name: spring-boot-app
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
# Wir biegen localhost auf den Service-Namen 'db' um
|
||||
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/xxx_sphere?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
|
||||
# Hier injizieren wir die Werte für deine Platzhalter
|
||||
- DB_USER=root
|
||||
- DB_PASSWORD=xxxsphere123!
|
||||
# Wartet kurz, bis die DB wirklich bereit ist (optional, aber empfohlen)
|
||||
restart: on-failure
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
@@ -238,7 +238,7 @@ public class VanillaGameController {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
// Max 2 Mitspieler (1 Host + max 1 Gast)
|
||||
if (session.getMitspieler().size() >= 1) {
|
||||
if (session.getMitspieler().size() >= 2) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
VanillaMitspielerEntity entity = new VanillaMitspielerEntity();
|
||||
|
||||
@@ -4,6 +4,7 @@ import de.oaa.xxx.games.common.aufgaben.Toy;
|
||||
import de.oaa.xxx.games.common.aufgaben.ToyPage;
|
||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.games.common.entity.ToyEntity;
|
||||
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.games.common.repository.GruppenAboRepository;
|
||||
import de.oaa.xxx.games.common.repository.ToyRepository;
|
||||
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
||||
@@ -33,6 +34,7 @@ import java.util.Base64;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -47,15 +49,18 @@ public class VanillaToyController {
|
||||
private final ToyRepository toyRepository;
|
||||
private final UserService userService;
|
||||
private final GruppenAboRepository aboRepository;
|
||||
private final AufgabenGruppeRepository gruppeRepository;
|
||||
private final SubscriptionLimitService limitService;
|
||||
|
||||
public VanillaToyController(ToyRepository toyRepository,
|
||||
UserService userService,
|
||||
GruppenAboRepository aboRepository,
|
||||
AufgabenGruppeRepository gruppeRepository,
|
||||
SubscriptionLimitService limitService) {
|
||||
this.toyRepository = toyRepository;
|
||||
this.userService = userService;
|
||||
this.aboRepository = aboRepository;
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.limitService = limitService;
|
||||
}
|
||||
|
||||
@@ -114,6 +119,30 @@ public class VanillaToyController {
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all distinct toys required by aufgaben and finisher of the given gruppe IDs.
|
||||
* Only vanilla-safe groups (no Strafen, no Sperren) are considered.
|
||||
*/
|
||||
@GetMapping("/required")
|
||||
public ResponseEntity<List<Toy>> required(@RequestParam List<UUID> gruppenIds) {
|
||||
Map<UUID, ToyEntity> toyMap = new java.util.LinkedHashMap<>();
|
||||
gruppeRepository.findAllById(gruppenIds).forEach(gruppe -> {
|
||||
gruppe.getAufgaben().forEach(a -> {
|
||||
if (a.getBenoetigteToys() != null)
|
||||
a.getBenoetigteToys().forEach(t -> toyMap.putIfAbsent(t.getToyId(), t));
|
||||
});
|
||||
gruppe.getFinisher().forEach(f -> {
|
||||
if (f.getBenoetigteToys() != null)
|
||||
f.getBenoetigteToys().forEach(t -> toyMap.putIfAbsent(t.getToyId(), t));
|
||||
});
|
||||
});
|
||||
List<Toy> result = toyMap.values().stream()
|
||||
.sorted(Comparator.comparing(ToyEntity::getName, String.CASE_INSENSITIVE_ORDER))
|
||||
.map(ToyEntity::toToy)
|
||||
.toList();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{toyId}")
|
||||
public ResponseEntity<Toy> get(@PathVariable UUID toyId) {
|
||||
return toyRepository.findById(toyId)
|
||||
|
||||
@@ -12,26 +12,28 @@ import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class MailService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(MailService.class);
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(MailService.class);
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
public MailService(JavaMailSender mailSender) {
|
||||
this.mailSender = mailSender;
|
||||
}
|
||||
public MailService(JavaMailSender mailSender) {
|
||||
this.mailSender = mailSender;
|
||||
}
|
||||
|
||||
public boolean send(Email email) {
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(email.getEmailAdresse()));
|
||||
message.setSubject(email.getTitel());
|
||||
message.setFrom(InternetAddress.parse("noreply@xxx-bdsmgame.de")[0]);
|
||||
message.setContent(email.getText(), "text/html; charset=utf-8");
|
||||
mailSender.send(message);
|
||||
return true;
|
||||
} catch (MessagingException e) {
|
||||
LOGGER.error(e.getLocalizedMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public boolean send(Email email) {
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(email.getEmailAdresse()));
|
||||
message.setSubject(email.getTitel());
|
||||
message.setFrom(InternetAddress.parse("noreply@xxx-sphere.de")[0]);
|
||||
message.setContent(email.getText(), "text/html; charset=utf-8");
|
||||
message.addHeader("X-Mailin-Tag", "no-tracking");
|
||||
message.addHeader("X-Sib-Attributes", "{\"X-SIB-TRACKING\":\"0\"}");
|
||||
mailSender.send(message);
|
||||
return true;
|
||||
} catch (MessagingException e) {
|
||||
LOGGER.error(e.getLocalizedMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,6 +347,7 @@ public class SocialController {
|
||||
user.getSichtbarkeitPinnwand(),
|
||||
user.getSichtbarkeitXp(),
|
||||
user.getSichtbarkeitLockhistorie(),
|
||||
user.getSichtbarkeitVorlieben(),
|
||||
user.isProfilBeiVeroeffentlichungenSichtbar());
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,13 @@ public record UserProfile(
|
||||
Sichtbarkeit sichtbarkeitPinnwand,
|
||||
Sichtbarkeit sichtbarkeitXp,
|
||||
Sichtbarkeit sichtbarkeitLockhistorie,
|
||||
Sichtbarkeit sichtbarkeitVorlieben,
|
||||
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, false);
|
||||
null, null, null, null, null, null, null, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ public class UserController {
|
||||
Sichtbarkeit sichtbarkeitPinnwand,
|
||||
Sichtbarkeit sichtbarkeitXp,
|
||||
Sichtbarkeit sichtbarkeitLockhistorie,
|
||||
Sichtbarkeit sichtbarkeitVorlieben,
|
||||
Boolean profilBeiVeroeffentlichungenSichtbar) {}
|
||||
|
||||
@PutMapping("/me/picture")
|
||||
@@ -138,6 +139,7 @@ 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.sichtbarkeitVorlieben() != null) user.setSichtbarkeitVorlieben(request.sichtbarkeitVorlieben());
|
||||
if (request.profilBeiVeroeffentlichungenSichtbar() != null) {
|
||||
boolean showAuthor = request.profilBeiVeroeffentlichungenSichtbar();
|
||||
user.setProfilBeiVeroeffentlichungenSichtbar(showAuthor);
|
||||
|
||||
@@ -92,6 +92,10 @@ public class UserEntity {
|
||||
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
|
||||
private Sichtbarkeit sichtbarkeitLockhistorie = Sichtbarkeit.ALLE;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
|
||||
private Sichtbarkeit sichtbarkeitVorlieben = Sichtbarkeit.ALLE;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||
private boolean profilBeiVeroeffentlichungenSichtbar = false;
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.oaa.xxx.vorlieben;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "user_vorliebe",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"userId", "itemId"}))
|
||||
public class UserVorliebeEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID itemId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 30)
|
||||
private VorliebeBewertung bewertung;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.oaa.xxx.vorlieben;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface UserVorliebeRepository extends JpaRepository<UserVorliebeEntity, UUID> {
|
||||
List<UserVorliebeEntity> findByUserId(UUID userId);
|
||||
Optional<UserVorliebeEntity> findByUserIdAndItemId(UUID userId, UUID itemId);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.oaa.xxx.vorlieben;
|
||||
|
||||
public enum VorliebeBewertung {
|
||||
GEHT_GAR_NICHT("Geht gar nicht"),
|
||||
EHER_NICHT("Eher nicht"),
|
||||
NEUTRAL("Neutral"),
|
||||
MAG_ICH("Mag ich"),
|
||||
UNBEDINGT("Unbedingt"),
|
||||
WILL_AUSPROBIEREN("Will ich ausprobieren");
|
||||
|
||||
private final String label;
|
||||
|
||||
VorliebeBewertung(String label) { this.label = label; }
|
||||
|
||||
public String getLabel() { return label; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.oaa.xxx.vorlieben;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "vorliebe_item")
|
||||
public class VorliebeItemEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID itemId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID kategorieId;
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "INT DEFAULT 0")
|
||||
private int sortOrder;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.oaa.xxx.vorlieben;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface VorliebeItemRepository extends JpaRepository<VorliebeItemEntity, UUID> {
|
||||
List<VorliebeItemEntity> findAllByOrderBySortOrderAscNameAsc();
|
||||
boolean existsByKategorieId(UUID kategorieId);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.oaa.xxx.vorlieben;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "vorliebe_kategorie")
|
||||
public class VorliebeKategorieEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID kategorieId;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "INT DEFAULT 0")
|
||||
private int sortOrder;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.oaa.xxx.vorlieben;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface VorliebeKategorieRepository extends JpaRepository<VorliebeKategorieEntity, UUID> {
|
||||
List<VorliebeKategorieEntity> findAllByOrderBySortOrderAscNameAsc();
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package de.oaa.xxx.vorlieben;
|
||||
|
||||
import de.oaa.xxx.admin.AdminEntity;
|
||||
import de.oaa.xxx.admin.AdminRepository;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/admin/vorlieben")
|
||||
@Transactional
|
||||
public class VorliebenAdminController {
|
||||
|
||||
private final VorliebeKategorieRepository kategorieRepository;
|
||||
private final VorliebeItemRepository itemRepository;
|
||||
private final UserVorliebeRepository userVorliebeRepository;
|
||||
private final AdminRepository adminRepository;
|
||||
private final UserService userService;
|
||||
|
||||
public VorliebenAdminController(VorliebeKategorieRepository kategorieRepository,
|
||||
VorliebeItemRepository itemRepository,
|
||||
UserVorliebeRepository userVorliebeRepository,
|
||||
AdminRepository adminRepository,
|
||||
UserService userService) {
|
||||
this.kategorieRepository = kategorieRepository;
|
||||
this.itemRepository = itemRepository;
|
||||
this.userVorliebeRepository = userVorliebeRepository;
|
||||
this.adminRepository = adminRepository;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
private AdminEntity requireAdmin(Principal principal) {
|
||||
var user = userService.requireUser(principal);
|
||||
return adminRepository.findByUserId(user.getUserId())
|
||||
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.FORBIDDEN, "Kein Admin"));
|
||||
}
|
||||
|
||||
record KategorieRequest(String name, int sortOrder) {}
|
||||
record ItemRequest(UUID kategorieId, String name, int sortOrder) {}
|
||||
record KategorieDto(UUID kategorieId, String name, int sortOrder) {}
|
||||
record ItemDto(UUID itemId, UUID kategorieId, String name, int sortOrder) {}
|
||||
|
||||
// ── Kategorien ────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/kategorien")
|
||||
public ResponseEntity<List<KategorieDto>> getKategorien(Principal principal) {
|
||||
requireAdmin(principal);
|
||||
List<KategorieDto> result = kategorieRepository.findAllByOrderBySortOrderAscNameAsc().stream()
|
||||
.map(k -> new KategorieDto(k.getKategorieId(), k.getName(), k.getSortOrder()))
|
||||
.toList();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/kategorien")
|
||||
public ResponseEntity<KategorieDto> createKategorie(@RequestBody KategorieRequest req, Principal principal) {
|
||||
requireAdmin(principal);
|
||||
if (req.name() == null || req.name().isBlank()) return ResponseEntity.badRequest().build();
|
||||
VorliebeKategorieEntity entity = new VorliebeKategorieEntity();
|
||||
entity.setKategorieId(UUID.randomUUID());
|
||||
entity.setName(req.name().trim());
|
||||
entity.setSortOrder(req.sortOrder());
|
||||
kategorieRepository.save(entity);
|
||||
return ResponseEntity.status(201).body(
|
||||
new KategorieDto(entity.getKategorieId(), entity.getName(), entity.getSortOrder()));
|
||||
}
|
||||
|
||||
@PutMapping("/kategorien/{id}")
|
||||
public ResponseEntity<Void> updateKategorie(@PathVariable UUID id,
|
||||
@RequestBody KategorieRequest req,
|
||||
Principal principal) {
|
||||
requireAdmin(principal);
|
||||
if (req.name() == null || req.name().isBlank()) return ResponseEntity.badRequest().build();
|
||||
VorliebeKategorieEntity entity = kategorieRepository.findById(id).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
entity.setName(req.name().trim());
|
||||
entity.setSortOrder(req.sortOrder());
|
||||
kategorieRepository.save(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/kategorien/{id}")
|
||||
public ResponseEntity<Void> deleteKategorie(@PathVariable UUID id, Principal principal) {
|
||||
requireAdmin(principal);
|
||||
if (!kategorieRepository.existsById(id)) return ResponseEntity.notFound().build();
|
||||
if (itemRepository.existsByKategorieId(id)) {
|
||||
return ResponseEntity.status(409)
|
||||
.header("X-Error", "has-items")
|
||||
.build();
|
||||
}
|
||||
kategorieRepository.deleteById(id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── Items ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/items")
|
||||
public ResponseEntity<List<ItemDto>> getItems(Principal principal) {
|
||||
requireAdmin(principal);
|
||||
List<ItemDto> result = itemRepository.findAllByOrderBySortOrderAscNameAsc().stream()
|
||||
.map(i -> new ItemDto(i.getItemId(), i.getKategorieId(), i.getName(), i.getSortOrder()))
|
||||
.toList();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/items")
|
||||
public ResponseEntity<ItemDto> createItem(@RequestBody ItemRequest req, Principal principal) {
|
||||
requireAdmin(principal);
|
||||
if (req.name() == null || req.name().isBlank() || req.kategorieId() == null)
|
||||
return ResponseEntity.badRequest().build();
|
||||
if (!kategorieRepository.existsById(req.kategorieId()))
|
||||
return ResponseEntity.status(422).header("X-Error", "kategorie-not-found").build();
|
||||
VorliebeItemEntity entity = new VorliebeItemEntity();
|
||||
entity.setItemId(UUID.randomUUID());
|
||||
entity.setKategorieId(req.kategorieId());
|
||||
entity.setName(req.name().trim());
|
||||
entity.setSortOrder(req.sortOrder());
|
||||
itemRepository.save(entity);
|
||||
return ResponseEntity.status(201).body(
|
||||
new ItemDto(entity.getItemId(), entity.getKategorieId(), entity.getName(), entity.getSortOrder()));
|
||||
}
|
||||
|
||||
@PutMapping("/items/{id}")
|
||||
public ResponseEntity<Void> updateItem(@PathVariable UUID id,
|
||||
@RequestBody ItemRequest req,
|
||||
Principal principal) {
|
||||
requireAdmin(principal);
|
||||
if (req.name() == null || req.name().isBlank()) return ResponseEntity.badRequest().build();
|
||||
VorliebeItemEntity entity = itemRepository.findById(id).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
if (req.kategorieId() != null && !kategorieRepository.existsById(req.kategorieId()))
|
||||
return ResponseEntity.status(422).header("X-Error", "kategorie-not-found").build();
|
||||
if (req.kategorieId() != null) entity.setKategorieId(req.kategorieId());
|
||||
entity.setName(req.name().trim());
|
||||
entity.setSortOrder(req.sortOrder());
|
||||
itemRepository.save(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/items/{id}")
|
||||
public ResponseEntity<Void> deleteItem(@PathVariable UUID id, Principal principal) {
|
||||
requireAdmin(principal);
|
||||
VorliebeItemEntity entity = itemRepository.findById(id).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
// Remove all user ratings for this item first
|
||||
List<UserVorliebeEntity> ratings = userVorliebeRepository.findAll().stream()
|
||||
.filter(uv -> uv.getItemId().equals(id)).toList();
|
||||
userVorliebeRepository.deleteAll(ratings);
|
||||
itemRepository.delete(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── Export ────────────────────────────────────────────────────────────────
|
||||
|
||||
record ExportKategorie(UUID kategorieId, String name, int sortOrder, List<ExportItem> items) {}
|
||||
record ExportItem(UUID itemId, String name, int sortOrder) {}
|
||||
|
||||
@GetMapping("/export")
|
||||
public ResponseEntity<List<ExportKategorie>> export(Principal principal) {
|
||||
requireAdmin(principal);
|
||||
List<VorliebeKategorieEntity> kategorien = kategorieRepository.findAllByOrderBySortOrderAscNameAsc();
|
||||
List<VorliebeItemEntity> allItems = itemRepository.findAllByOrderBySortOrderAscNameAsc();
|
||||
Map<UUID, List<VorliebeItemEntity>> byKat = allItems.stream()
|
||||
.collect(Collectors.groupingBy(VorliebeItemEntity::getKategorieId));
|
||||
|
||||
List<ExportKategorie> result = kategorien.stream()
|
||||
.map(k -> new ExportKategorie(
|
||||
k.getKategorieId(), k.getName(), k.getSortOrder(),
|
||||
byKat.getOrDefault(k.getKategorieId(), List.of()).stream()
|
||||
.map(i -> new ExportItem(i.getItemId(), i.getName(), i.getSortOrder()))
|
||||
.toList()))
|
||||
.toList();
|
||||
return ResponseEntity.ok()
|
||||
.header("Content-Disposition", "attachment; filename=\"vorlieben-export.json\"")
|
||||
.body(result);
|
||||
}
|
||||
|
||||
// ── Import ────────────────────────────────────────────────────────────────
|
||||
|
||||
record ImportItem(String name, int sortOrder) {}
|
||||
record ImportKategorie(String name, int sortOrder, List<ImportItem> items) {}
|
||||
record ImportResult(int kategorienCreated, int kategorienSkipped, int itemsCreated, int itemsSkipped) {}
|
||||
|
||||
@PostMapping("/import")
|
||||
public ResponseEntity<ImportResult> importData(@RequestBody List<ImportKategorie> data,
|
||||
Principal principal) {
|
||||
requireAdmin(principal);
|
||||
if (data == null) return ResponseEntity.badRequest().build();
|
||||
|
||||
int kCreated = 0, kSkipped = 0, iCreated = 0, iSkipped = 0;
|
||||
List<VorliebeKategorieEntity> existingKategorien = kategorieRepository.findAllByOrderBySortOrderAscNameAsc();
|
||||
Map<String, VorliebeKategorieEntity> byName = existingKategorien.stream()
|
||||
.collect(Collectors.toMap(k -> k.getName().toLowerCase(), k -> k,
|
||||
(a, b) -> a));
|
||||
|
||||
for (ImportKategorie ik : data) {
|
||||
if (ik.name() == null || ik.name().isBlank()) { kSkipped++; continue; }
|
||||
VorliebeKategorieEntity kat = byName.get(ik.name().trim().toLowerCase());
|
||||
if (kat == null) {
|
||||
kat = new VorliebeKategorieEntity();
|
||||
kat.setKategorieId(UUID.randomUUID());
|
||||
kat.setName(ik.name().trim());
|
||||
kat.setSortOrder(ik.sortOrder());
|
||||
kategorieRepository.save(kat);
|
||||
byName.put(kat.getName().toLowerCase(), kat);
|
||||
kCreated++;
|
||||
} else {
|
||||
kSkipped++;
|
||||
}
|
||||
|
||||
if (ik.items() == null) continue;
|
||||
final UUID katId = kat.getKategorieId();
|
||||
List<VorliebeItemEntity> existingItems =
|
||||
itemRepository.findAllByOrderBySortOrderAscNameAsc().stream()
|
||||
.filter(i -> i.getKategorieId().equals(katId))
|
||||
.toList();
|
||||
Set<String> existingNames = existingItems.stream()
|
||||
.map(i -> i.getName().toLowerCase()).collect(Collectors.toSet());
|
||||
|
||||
for (ImportItem ii : ik.items()) {
|
||||
if (ii.name() == null || ii.name().isBlank()) { iSkipped++; continue; }
|
||||
if (existingNames.contains(ii.name().trim().toLowerCase())) { iSkipped++; continue; }
|
||||
VorliebeItemEntity item = new VorliebeItemEntity();
|
||||
item.setItemId(UUID.randomUUID());
|
||||
item.setKategorieId(katId);
|
||||
item.setName(ii.name().trim());
|
||||
item.setSortOrder(ii.sortOrder());
|
||||
itemRepository.save(item);
|
||||
iCreated++;
|
||||
}
|
||||
}
|
||||
return ResponseEntity.ok(new ImportResult(kCreated, kSkipped, iCreated, iSkipped));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package de.oaa.xxx.vorlieben;
|
||||
|
||||
import de.oaa.xxx.social.entity.FriendshipEntity;
|
||||
import de.oaa.xxx.social.repository.FriendshipRepository;
|
||||
import de.oaa.xxx.user.Sichtbarkeit;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/vorlieben")
|
||||
@Transactional
|
||||
public class VorliebenController {
|
||||
|
||||
private final VorliebeKategorieRepository kategorieRepository;
|
||||
private final VorliebeItemRepository itemRepository;
|
||||
private final UserVorliebeRepository userVorliebeRepository;
|
||||
private final UserService userService;
|
||||
private final UserRepository userRepository;
|
||||
private final FriendshipRepository friendshipRepository;
|
||||
|
||||
public VorliebenController(VorliebeKategorieRepository kategorieRepository,
|
||||
VorliebeItemRepository itemRepository,
|
||||
UserVorliebeRepository userVorliebeRepository,
|
||||
UserService userService,
|
||||
UserRepository userRepository,
|
||||
FriendshipRepository friendshipRepository) {
|
||||
this.kategorieRepository = kategorieRepository;
|
||||
this.itemRepository = itemRepository;
|
||||
this.userVorliebeRepository = userVorliebeRepository;
|
||||
this.userService = userService;
|
||||
this.userRepository = userRepository;
|
||||
this.friendshipRepository = friendshipRepository;
|
||||
}
|
||||
|
||||
record ItemDto(UUID itemId, String name, int sortOrder) {}
|
||||
record KategorieWithItems(UUID kategorieId, String name, int sortOrder, List<ItemDto> items) {}
|
||||
|
||||
/** Returns all categories with their items – used by the profile edit page. */
|
||||
@GetMapping("/items")
|
||||
public ResponseEntity<List<KategorieWithItems>> getItems() {
|
||||
List<VorliebeKategorieEntity> kategorien = kategorieRepository.findAllByOrderBySortOrderAscNameAsc();
|
||||
List<VorliebeItemEntity> allItems = itemRepository.findAllByOrderBySortOrderAscNameAsc();
|
||||
|
||||
Map<UUID, List<VorliebeItemEntity>> byKategorie = allItems.stream()
|
||||
.collect(Collectors.groupingBy(VorliebeItemEntity::getKategorieId));
|
||||
|
||||
List<KategorieWithItems> result = kategorien.stream()
|
||||
.map(k -> new KategorieWithItems(
|
||||
k.getKategorieId(), k.getName(), k.getSortOrder(),
|
||||
byKategorie.getOrDefault(k.getKategorieId(), List.of()).stream()
|
||||
.map(i -> new ItemDto(i.getItemId(), i.getName(), i.getSortOrder()))
|
||||
.toList()))
|
||||
.filter(k -> !k.items().isEmpty())
|
||||
.toList();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/** Returns the current user's ratings as a map of itemId → bewertung name. */
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<Map<String, String>> getMyVorlieben(Principal principal) {
|
||||
UUID userId = userService.requireUser(principal).getUserId();
|
||||
Map<String, String> result = userVorliebeRepository.findByUserId(userId).stream()
|
||||
.collect(Collectors.toMap(
|
||||
uv -> uv.getItemId().toString(),
|
||||
uv -> uv.getBewertung().name()));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/** Saves the current user's ratings. Value null or blank removes the rating. */
|
||||
@PutMapping("/me")
|
||||
public ResponseEntity<Void> saveMyVorlieben(@RequestBody Map<String, String> ratings, Principal principal) {
|
||||
UUID userId = userService.requireUser(principal).getUserId();
|
||||
for (var entry : ratings.entrySet()) {
|
||||
UUID itemId;
|
||||
try { itemId = UUID.fromString(entry.getKey()); }
|
||||
catch (IllegalArgumentException e) { continue; }
|
||||
|
||||
String bewertungStr = entry.getValue();
|
||||
if (bewertungStr == null || bewertungStr.isBlank()) {
|
||||
userVorliebeRepository.findByUserIdAndItemId(userId, itemId)
|
||||
.ifPresent(userVorliebeRepository::delete);
|
||||
} else {
|
||||
VorliebeBewertung bewertung;
|
||||
try { bewertung = VorliebeBewertung.valueOf(bewertungStr); }
|
||||
catch (IllegalArgumentException e) { continue; }
|
||||
|
||||
UserVorliebeEntity uv = userVorliebeRepository.findByUserIdAndItemId(userId, itemId)
|
||||
.orElseGet(() -> {
|
||||
UserVorliebeEntity n = new UserVorliebeEntity();
|
||||
n.setId(UUID.randomUUID());
|
||||
n.setUserId(userId);
|
||||
n.setItemId(itemId);
|
||||
return n;
|
||||
});
|
||||
uv.setBewertung(bewertung);
|
||||
userVorliebeRepository.save(uv);
|
||||
}
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
/** Returns another user's ratings, respecting their privacy setting. */
|
||||
@GetMapping("/user/{userId}")
|
||||
public ResponseEntity<Map<String, Object>> getUserVorlieben(
|
||||
@PathVariable UUID userId, Principal principal) {
|
||||
UserEntity targetUser = userRepository.findById(userId).orElse(null);
|
||||
if (targetUser == null) return ResponseEntity.notFound().build();
|
||||
|
||||
boolean isOwn = false;
|
||||
boolean isFriend = false;
|
||||
if (principal != null) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
isOwn = myId.equals(userId);
|
||||
if (!isOwn) {
|
||||
Optional<FriendshipEntity> f = friendshipRepository.findExisting(myId, userId);
|
||||
isFriend = f.isPresent() && f.get().getStatus() == FriendshipEntity.Status.ACCEPTED;
|
||||
}
|
||||
}
|
||||
|
||||
Sichtbarkeit sv = targetUser.getSichtbarkeitVorlieben();
|
||||
if (sv == null) sv = Sichtbarkeit.ALLE;
|
||||
|
||||
boolean canSee = isOwn
|
||||
|| sv == Sichtbarkeit.ALLE
|
||||
|| (sv == Sichtbarkeit.NUR_FREUNDE && isFriend);
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("sichtbarkeit", sv.name());
|
||||
result.put("canSee", canSee);
|
||||
|
||||
if (canSee) {
|
||||
Map<String, String> ratingsMap = userVorliebeRepository.findByUserId(userId).stream()
|
||||
.collect(Collectors.toMap(
|
||||
uv -> uv.getItemId().toString(),
|
||||
uv -> uv.getBewertung().name()));
|
||||
result.put("ratings", ratingsMap);
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,12 @@ spring.jpa.properties.hibernate.type.preferred_uuid_jdbc_type=VARCHAR
|
||||
#spring.mail.properties.mail.smtp.starttls.enable=false
|
||||
|
||||
# Mailpit
|
||||
spring.mail.host=localhost
|
||||
spring.mail.port=1025
|
||||
spring.mail.username=
|
||||
spring.mail.password=
|
||||
spring.mail.properties.mail.smtp.auth=false
|
||||
spring.mail.properties.mail.smtp.starttls.enable=false
|
||||
spring.mail.host=smtp-relay.brevo.com
|
||||
spring.mail.port=587
|
||||
spring.mail.username=a6b17a001@smtp-brevo.com
|
||||
spring.mail.password=xsmtpsib-77b691d562154574133d12b09d44a06e166d30091aac6642480771a0ae463a79-8yH3jHOd4nMMAwuS
|
||||
spring.mail.properties.mail.smtp.auth=true
|
||||
spring.mail.properties.mail.smtp.starttls.enable=true
|
||||
|
||||
# JWT Keystore
|
||||
jwt.keystore.path=classpath:xxx.jks
|
||||
|
||||
13
xxxthegame/src/main/resources/sql/admin.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
● -- Person zur admin-Tabelle als SUPERADMIN hinzufügen (über E-Mail-Adresse)
|
||||
INSERT INTO admin (admin_id, user_id, rolle, created_at)
|
||||
SELECT UUID(), u.user_id, 'SUPERADMIN', NOW()
|
||||
FROM user u
|
||||
WHERE u.email = 'email@beispiel.de';
|
||||
|
||||
-- Falls der User bereits ein (normaler) Admin ist, Rolle upgraden:
|
||||
UPDATE admin a
|
||||
JOIN user u ON a.user_id = u.user_id
|
||||
SET a.rolle = 'SUPERADMIN'
|
||||
WHERE u.email = 'email@beispiel.de';
|
||||
|
||||
--Einfach email@beispiel.de durch die Ziel-E-Mail ersetzen. Das erste Statement fügt einen neuen Admin-Eintrag ein, das zweite upgraded einen bestehenden. Nur eines von beiden ausführen je nach Fall.
|
||||
@@ -465,6 +465,7 @@
|
||||
<button class="tab-btn" data-tab="feedback">Feedback</button>
|
||||
<button class="tab-btn" data-tab="aufgabengruppen">Aufgabengruppen</button>
|
||||
<button class="tab-btn" data-tab="toys">Toys</button>
|
||||
<button class="tab-btn" data-tab="vorlieben">Vorlieben</button>
|
||||
<button class="tab-btn superadmin-only" data-tab="admins">Admins</button>
|
||||
<button class="tab-btn superadmin-only" data-tab="abonnements">Abonnements</button>
|
||||
<button class="tab-btn superadmin-only" data-tab="schnittstellen">Schnittstellen</button>
|
||||
@@ -580,6 +581,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Vorlieben ── -->
|
||||
<div class="tab-panel" id="panel-vorlieben">
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Vorlieben</h2>
|
||||
<div class="section-actions">
|
||||
<button class="btn-action" id="vlExportBtn">⬇ Export</button>
|
||||
<button class="btn-action" id="vlImportBtn">⬆ Import</button>
|
||||
<input type="file" id="vlImportFile" accept=".json" style="display:none">
|
||||
<button class="btn-add" id="vlKatCreateBtn">+ Neue Kategorie</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-error" id="vlError"></div>
|
||||
<!-- Kategorie-Formular -->
|
||||
<div class="form-section" id="vlKatForm" style="display:none;">
|
||||
<h3 id="vlKatFormTitle">Kategorie anlegen</h3>
|
||||
<div class="form-row">
|
||||
<input type="hidden" id="vlKatId">
|
||||
<input type="text" id="vlKatName" placeholder="Name der Kategorie">
|
||||
<input type="number" id="vlKatSort" placeholder="Reihenfolge (0=oben)" value="0" min="0" style="flex:0 0 160px;">
|
||||
<button onclick="saveKategorie()">Speichern</button>
|
||||
<button class="secondary" onclick="cancelKategorie()">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Item-Formular -->
|
||||
<div class="form-section" id="vlItemForm" style="display:none;">
|
||||
<h3 id="vlItemFormTitle">Vorliebe anlegen</h3>
|
||||
<div class="form-row">
|
||||
<input type="hidden" id="vlItemId">
|
||||
<select id="vlItemKat" style="flex:0 0 220px;"></select>
|
||||
<input type="text" id="vlItemName" placeholder="Name der Vorliebe">
|
||||
<input type="number" id="vlItemSort" placeholder="Reihenfolge" value="0" min="0" style="flex:0 0 140px;">
|
||||
<button onclick="saveItem()">Speichern</button>
|
||||
<button class="secondary" onclick="cancelItem()">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Kategorien-Liste -->
|
||||
<div class="gruppe-list" id="vlKatList"><p class="empty-hint">Wird geladen…</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Admins (nur Superadmin) ── -->
|
||||
<div class="tab-panel superadmin-only" id="panel-admins">
|
||||
<div class="form-section">
|
||||
@@ -2337,6 +2379,225 @@ function escAdminHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
// ── Vorlieben-Verwaltung ───────────────────────────────────────────────────
|
||||
|
||||
let _vlKategorien = [];
|
||||
let _vlItems = [];
|
||||
|
||||
async function loadVorliebenAdmin() {
|
||||
const [rKat, rItem] = await Promise.all([
|
||||
fetch('/admin/vorlieben/kategorien'),
|
||||
fetch('/admin/vorlieben/items'),
|
||||
]);
|
||||
if (!rKat.ok || !rItem.ok) return;
|
||||
_vlKategorien = await rKat.json();
|
||||
_vlItems = await rItem.json();
|
||||
renderVlListe();
|
||||
renderVlKatDropdown();
|
||||
}
|
||||
|
||||
function renderVlListe() {
|
||||
const container = document.getElementById('vlKatList');
|
||||
if (!_vlKategorien.length) {
|
||||
container.innerHTML = '<p class="empty-hint">Keine Kategorien vorhanden. Lege zuerst eine Kategorie an.</p>';
|
||||
return;
|
||||
}
|
||||
const itemsByKat = {};
|
||||
_vlKategorien.forEach(k => { itemsByKat[k.kategorieId] = []; });
|
||||
_vlItems.forEach(i => { if (itemsByKat[i.kategorieId]) itemsByKat[i.kategorieId].push(i); });
|
||||
|
||||
container.innerHTML = _vlKategorien.map(k => {
|
||||
const items = itemsByKat[k.kategorieId] || [];
|
||||
const itemsHtml = items.length
|
||||
? `<div class="item-list">${items.map(i => `
|
||||
<div class="item">
|
||||
<div class="item-row" style="cursor:default;">
|
||||
<span class="item-text">${escAdminHtml(i.name)}</span>
|
||||
<span style="font-size:0.72rem;color:var(--color-muted);flex-shrink:0;margin-right:0.5rem;">#${i.sortOrder}</span>
|
||||
<div class="item-badges" style="flex-shrink:0;">
|
||||
<button class="btn-item-edit" onclick="editItem('${i.itemId}')">✎</button>
|
||||
<button class="btn-item-delete" onclick="deleteItem('${i.itemId}','${escAdminHtml(i.name).replace(/'/g,"\\'")}')">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).join('')}</div>`
|
||||
: `<p class="sub-empty">Keine Vorlieben in dieser Kategorie.</p>`;
|
||||
|
||||
return `
|
||||
<div class="gruppe-card open" id="vlkat-${k.kategorieId}">
|
||||
<div class="gruppe-header" onclick="this.closest('.gruppe-card').classList.toggle('open')">
|
||||
<div class="gruppe-meta">
|
||||
<div class="gruppe-name">${escAdminHtml(k.name)}</div>
|
||||
<div class="gruppe-info">${items.length} Vorliebe${items.length !== 1 ? 'n' : ''} · Reihenfolge: ${k.sortOrder}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.4rem;align-items:center;flex-shrink:0;">
|
||||
<button class="btn-item-edit" onclick="event.stopPropagation();addItemToKat('${k.kategorieId}')">+ Vorliebe</button>
|
||||
<button class="btn-item-edit" onclick="event.stopPropagation();editKategorie('${k.kategorieId}')">✎</button>
|
||||
<button class="btn-item-delete" onclick="event.stopPropagation();deleteKategorie('${k.kategorieId}','${escAdminHtml(k.name).replace(/'/g,"\\'")}')">✕</button>
|
||||
<span class="gruppe-toggle">▶</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gruppe-body">
|
||||
<div class="sub-section">
|
||||
<div class="sub-section-header">
|
||||
<span class="sub-section-title">Vorlieben</span>
|
||||
</div>
|
||||
${itemsHtml}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderVlKatDropdown() {
|
||||
const sel = document.getElementById('vlItemKat');
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = _vlKategorien.map(k =>
|
||||
`<option value="${k.kategorieId}">${escAdminHtml(k.name)}</option>`).join('');
|
||||
if (cur) sel.value = cur;
|
||||
}
|
||||
|
||||
// ── Kategorien CRUD ──
|
||||
document.getElementById('vlKatCreateBtn').addEventListener('click', () => {
|
||||
document.getElementById('vlKatId').value = '';
|
||||
document.getElementById('vlKatName').value = '';
|
||||
document.getElementById('vlKatSort').value = '0';
|
||||
document.getElementById('vlKatFormTitle').textContent = 'Kategorie anlegen';
|
||||
document.getElementById('vlItemForm').style.display = 'none';
|
||||
document.getElementById('vlKatForm').style.display = '';
|
||||
document.getElementById('vlKatName').focus();
|
||||
});
|
||||
|
||||
function cancelKategorie() { document.getElementById('vlKatForm').style.display = 'none'; }
|
||||
|
||||
function editKategorie(id) {
|
||||
const k = _vlKategorien.find(x => x.kategorieId === id);
|
||||
if (!k) return;
|
||||
document.getElementById('vlKatId').value = k.kategorieId;
|
||||
document.getElementById('vlKatName').value = k.name;
|
||||
document.getElementById('vlKatSort').value = k.sortOrder;
|
||||
document.getElementById('vlKatFormTitle').textContent = 'Kategorie bearbeiten';
|
||||
document.getElementById('vlItemForm').style.display = 'none';
|
||||
document.getElementById('vlKatForm').style.display = '';
|
||||
document.getElementById('vlKatName').focus();
|
||||
}
|
||||
|
||||
async function saveKategorie() {
|
||||
const id = document.getElementById('vlKatId').value;
|
||||
const name = document.getElementById('vlKatName').value.trim();
|
||||
const sort = parseInt(document.getElementById('vlKatSort').value) || 0;
|
||||
const errEl = document.getElementById('vlError');
|
||||
if (!name) { errEl.textContent = 'Name darf nicht leer sein.'; return; }
|
||||
const url = id ? `/admin/vorlieben/kategorien/${id}` : '/admin/vorlieben/kategorien';
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
const r = await fetch(url, { method, headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ name, sortOrder: sort }) });
|
||||
if (r.ok || r.status === 201) {
|
||||
errEl.textContent = '';
|
||||
document.getElementById('vlKatForm').style.display = 'none';
|
||||
await loadVorliebenAdmin();
|
||||
} else { errEl.textContent = 'Fehler beim Speichern.'; }
|
||||
}
|
||||
|
||||
async function deleteKategorie(id, name) {
|
||||
if (!confirm(`Kategorie "${name}" löschen?`)) return;
|
||||
const errEl = document.getElementById('vlError');
|
||||
const r = await fetch(`/admin/vorlieben/kategorien/${id}`, { method: 'DELETE' });
|
||||
if (r.ok) { await loadVorliebenAdmin(); }
|
||||
else if (r.status === 409) { errEl.textContent = 'Kategorie enthält noch Vorlieben – bitte zuerst alle Vorlieben dieser Kategorie löschen.'; }
|
||||
else { errEl.textContent = 'Fehler beim Löschen.'; }
|
||||
}
|
||||
|
||||
// ── Items CRUD ──
|
||||
document.getElementById('vlKatCreateBtn'); // already bound above
|
||||
|
||||
function addItemToKat(katId) {
|
||||
document.getElementById('vlItemId').value = '';
|
||||
document.getElementById('vlItemKat').value = katId;
|
||||
document.getElementById('vlItemName').value = '';
|
||||
document.getElementById('vlItemSort').value = '0';
|
||||
document.getElementById('vlItemFormTitle').textContent = 'Vorliebe anlegen';
|
||||
document.getElementById('vlKatForm').style.display = 'none';
|
||||
document.getElementById('vlItemForm').style.display = '';
|
||||
document.getElementById('vlItemName').focus();
|
||||
}
|
||||
|
||||
function cancelItem() { document.getElementById('vlItemForm').style.display = 'none'; }
|
||||
|
||||
function editItem(id) {
|
||||
const i = _vlItems.find(x => x.itemId === id);
|
||||
if (!i) return;
|
||||
document.getElementById('vlItemId').value = i.itemId;
|
||||
document.getElementById('vlItemKat').value = i.kategorieId;
|
||||
document.getElementById('vlItemName').value = i.name;
|
||||
document.getElementById('vlItemSort').value = i.sortOrder;
|
||||
document.getElementById('vlItemFormTitle').textContent = 'Vorliebe bearbeiten';
|
||||
document.getElementById('vlKatForm').style.display = 'none';
|
||||
document.getElementById('vlItemForm').style.display = '';
|
||||
document.getElementById('vlItemName').focus();
|
||||
}
|
||||
|
||||
async function saveItem() {
|
||||
const id = document.getElementById('vlItemId').value;
|
||||
const katId = document.getElementById('vlItemKat').value;
|
||||
const name = document.getElementById('vlItemName').value.trim();
|
||||
const sort = parseInt(document.getElementById('vlItemSort').value) || 0;
|
||||
const errEl = document.getElementById('vlError');
|
||||
if (!name || !katId) { errEl.textContent = 'Name und Kategorie sind erforderlich.'; return; }
|
||||
const url = id ? `/admin/vorlieben/items/${id}` : '/admin/vorlieben/items';
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
const r = await fetch(url, { method, headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ kategorieId: katId, name, sortOrder: sort }) });
|
||||
if (r.ok || r.status === 201) {
|
||||
errEl.textContent = '';
|
||||
document.getElementById('vlItemForm').style.display = 'none';
|
||||
await loadVorliebenAdmin();
|
||||
} else { errEl.textContent = 'Fehler beim Speichern.'; }
|
||||
}
|
||||
|
||||
async function deleteItem(id, name) {
|
||||
if (!confirm(`Vorliebe "${name}" löschen? Alle Nutzerbewertungen werden ebenfalls gelöscht.`)) return;
|
||||
const errEl = document.getElementById('vlError');
|
||||
const r = await fetch(`/admin/vorlieben/items/${id}`, { method: 'DELETE' });
|
||||
if (r.ok) { await loadVorliebenAdmin(); }
|
||||
else { errEl.textContent = 'Fehler beim Löschen.'; }
|
||||
}
|
||||
|
||||
// ── Export / Import ──
|
||||
document.getElementById('vlExportBtn').addEventListener('click', async () => {
|
||||
const r = await fetch('/admin/vorlieben/export');
|
||||
if (!r.ok) { document.getElementById('vlError').textContent = 'Export fehlgeschlagen.'; return; }
|
||||
const blob = await r.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a'); a.href = url; a.download = 'vorlieben-export.json';
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
const vlImportFile = document.getElementById('vlImportFile');
|
||||
document.getElementById('vlImportBtn').addEventListener('click', () => vlImportFile.click());
|
||||
vlImportFile.addEventListener('change', async function () {
|
||||
if (!this.files.length) return;
|
||||
const file = this.files[0]; this.value = '';
|
||||
const errEl = document.getElementById('vlError');
|
||||
errEl.style.color = 'var(--color-muted)'; errEl.textContent = 'Importiere…';
|
||||
let data;
|
||||
try { data = JSON.parse(await file.text()); }
|
||||
catch(e) { errEl.style.color = ''; errEl.textContent = 'Fehler: Ungültige JSON-Datei.'; return; }
|
||||
const r = await fetch('/admin/vorlieben/import', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data)
|
||||
});
|
||||
if (!r.ok) { errEl.style.color = ''; errEl.textContent = 'Import fehlgeschlagen.'; return; }
|
||||
const res = await r.json();
|
||||
await loadVorliebenAdmin();
|
||||
errEl.style.color = 'var(--color-success,#2ecc71)';
|
||||
errEl.textContent = `Import: ${res.kategorienCreated} Kategorien neu, ${res.itemsCreated} Vorlieben neu, ${res.kategorienSkipped + res.itemsSkipped} übersprungen.`;
|
||||
setTimeout(() => { errEl.textContent = ''; errEl.style.color = ''; }, 5000);
|
||||
});
|
||||
|
||||
// Vorlieben-Tab beim ersten Öffnen laden
|
||||
document.querySelector('.tab-btn[data-tab="vorlieben"]')?.addEventListener('click', () => {
|
||||
if (!_vlKategorien.length && !_vlItems.length) loadVorliebenAdmin();
|
||||
});
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -347,6 +347,27 @@
|
||||
.profil-tab-panel { display:none; }
|
||||
.profil-tab-panel.active { display:block; }
|
||||
|
||||
/* ── Vorlieben Tab ── */
|
||||
.vorlieben-group { margin-bottom:1.25rem; }
|
||||
.vorlieben-group-title {
|
||||
font-size:0.78rem; font-weight:700; color:var(--color-muted);
|
||||
text-transform:uppercase; letter-spacing:0.05em;
|
||||
margin-bottom:0.5rem; padding-bottom:0.3rem;
|
||||
border-bottom:1px solid var(--color-secondary);
|
||||
}
|
||||
.vorlieben-chips { display:flex; flex-wrap:wrap; gap:0.4rem; }
|
||||
.vorliebe-chip {
|
||||
display:inline-block; padding:0.25rem 0.65rem; border-radius:999px;
|
||||
font-size:0.82rem; border:1px solid var(--color-secondary);
|
||||
background:var(--color-card); color:var(--color-text);
|
||||
}
|
||||
.vorliebe-chip.bw-UNBEDINGT { border-color:#2e7d32; color:#2e7d32; }
|
||||
.vorliebe-chip.bw-MAG_ICH { border-color:#81c784; color:#81c784; }
|
||||
.vorliebe-chip.bw-WILL_AUSPROBIEREN{ border-color:#1e88e5; color:#1e88e5; }
|
||||
.vorliebe-chip.bw-NEUTRAL { border-color:#fdd835; color:#fdd835; }
|
||||
.vorliebe-chip.bw-EHER_NICHT { border-color:#fb8c00; color:#fb8c00; }
|
||||
.vorliebe-chip.bw-GEHT_GAR_NICHT { border-color:#e53935; color:#e53935; }
|
||||
|
||||
/* ── Post cards (profile posts tab) ── */
|
||||
.post-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:0.9rem; }
|
||||
.post-header { display:flex; align-items:center; gap:0.7rem; margin-bottom:0.6rem; }
|
||||
@@ -434,10 +455,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs: Feed | Pinnwand | Spielhistorie | Keyholder-Angebote -->
|
||||
<!-- Tabs: Feed | Pinnwand | Vorlieben | Spielhistorie | Keyholder-Angebote -->
|
||||
<div class="profil-tabs" style="margin-top:1.25rem;">
|
||||
<button class="profil-tab-btn active" id="tabBtnPosts" onclick="switchProfilTab('posts', this)">Feed</button>
|
||||
<button class="profil-tab-btn" id="tabBtnPinnwand" onclick="switchProfilTab('pinnwand', this)">Pinnwand</button>
|
||||
<button class="profil-tab-btn" id="tabBtnVorlieben" onclick="switchProfilTab('vorlieben', this)" style="display:none;">Vorlieben</button>
|
||||
<button class="profil-tab-btn" id="tabBtnGameHistory" onclick="switchProfilTab('gamehistory', this)">Spielhistorie</button>
|
||||
<button class="profil-tab-btn" id="tabBtnKhOffers" onclick="switchProfilTab('khoffers', this)">Keyholder-Angebote</button>
|
||||
</div>
|
||||
@@ -458,6 +480,11 @@
|
||||
<div id="pinnwandList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Vorlieben Tab -->
|
||||
<div class="profil-tab-panel" id="tab-vorlieben">
|
||||
<div id="vorliebenDisplay" style="margin-top:0.75rem;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Spielhistorie Tab -->
|
||||
<div class="profil-tab-panel" id="tab-gamehistory">
|
||||
<div id="gameHistoryList" style="margin-top:0.75rem;"></div>
|
||||
@@ -681,17 +708,20 @@
|
||||
}
|
||||
|
||||
function applyTabPrivacy(profile, isFriend) {
|
||||
const showFeed = canSee(profile.sichtbarkeitFeed, isFriend, isOwnProfile);
|
||||
const showPinnwand = canSee(profile.sichtbarkeitPinnwand, isFriend, isOwnProfile);
|
||||
const showHistory = canSee(profile.sichtbarkeitLockhistorie, isFriend, isOwnProfile);
|
||||
const showFeed = canSee(profile.sichtbarkeitFeed, isFriend, isOwnProfile);
|
||||
const showPinnwand = canSee(profile.sichtbarkeitPinnwand, isFriend, isOwnProfile);
|
||||
const showHistory = canSee(profile.sichtbarkeitLockhistorie, isFriend, isOwnProfile);
|
||||
const showVorlieben = canSee(profile.sichtbarkeitVorlieben, isFriend, isOwnProfile);
|
||||
|
||||
const btnFeed = document.getElementById('tabBtnPosts');
|
||||
const btnPinnwand = document.getElementById('tabBtnPinnwand');
|
||||
const btnHistory = document.getElementById('tabBtnGameHistory');
|
||||
const btnFeed = document.getElementById('tabBtnPosts');
|
||||
const btnPinnwand = document.getElementById('tabBtnPinnwand');
|
||||
const btnHistory = document.getElementById('tabBtnGameHistory');
|
||||
const btnVorlieben = document.getElementById('tabBtnVorlieben');
|
||||
|
||||
if (!showFeed) { btnFeed.style.display = 'none'; document.getElementById('tab-posts').classList.remove('active'); }
|
||||
if (!showPinnwand) { btnPinnwand.style.display = 'none'; }
|
||||
if (!showHistory) { btnHistory.style.display = 'none'; }
|
||||
if (showVorlieben) { btnVorlieben.style.display = ''; loadVorlieben(); }
|
||||
|
||||
// Ersten sichtbaren Tab aktivieren
|
||||
if (!showFeed) {
|
||||
@@ -707,6 +737,64 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Vorlieben anzeigen ──
|
||||
const BEWERTUNG_ORDER = ['UNBEDINGT','MAG_ICH','WILL_AUSPROBIEREN','NEUTRAL','EHER_NICHT','GEHT_GAR_NICHT'];
|
||||
const BEWERTUNG_LABEL = {
|
||||
UNBEDINGT: 'Unbedingt', MAG_ICH: 'Mag ich',
|
||||
WILL_AUSPROBIEREN: 'Will ich ausprobieren', NEUTRAL: 'Neutral',
|
||||
EHER_NICHT: 'Eher nicht', GEHT_GAR_NICHT: 'Geht gar nicht',
|
||||
};
|
||||
let _vorliebenLoaded = false;
|
||||
|
||||
async function loadVorlieben() {
|
||||
if (_vorliebenLoaded) return;
|
||||
_vorliebenLoaded = true;
|
||||
const container = document.getElementById('vorliebenDisplay');
|
||||
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Wird geladen…</p>';
|
||||
try {
|
||||
const [dataRes, itemsRes] = await Promise.all([
|
||||
fetch('/vorlieben/user/' + targetUserId),
|
||||
fetch('/vorlieben/items'),
|
||||
]);
|
||||
if (!dataRes.ok || !itemsRes.ok) { container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Fehler beim Laden.</p>'; return; }
|
||||
const data = await dataRes.json();
|
||||
const kategorien = await itemsRes.json();
|
||||
|
||||
if (!data.canSee) {
|
||||
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Nicht sichtbar.</p>'; return;
|
||||
}
|
||||
const ratings = data.ratings || {};
|
||||
|
||||
// Build itemId → name map
|
||||
const itemNames = {};
|
||||
kategorien.forEach(k => k.items.forEach(i => { itemNames[i.itemId] = i.name; }));
|
||||
|
||||
// Group by bewertung
|
||||
const grouped = {};
|
||||
Object.entries(ratings).forEach(([itemId, bw]) => {
|
||||
if (!grouped[bw]) grouped[bw] = [];
|
||||
grouped[bw].push(itemNames[itemId] || itemId);
|
||||
});
|
||||
|
||||
const visibleGroups = BEWERTUNG_ORDER.filter(bw => grouped[bw]?.length);
|
||||
if (!visibleGroups.length) {
|
||||
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Keine Vorlieben angegeben.</p>'; return;
|
||||
}
|
||||
|
||||
container.innerHTML = visibleGroups.map(bw => `
|
||||
<div class="vorlieben-group">
|
||||
<div class="vorlieben-group-title">${BEWERTUNG_LABEL[bw]}</div>
|
||||
<div class="vorlieben-chips">
|
||||
${grouped[bw].map(name =>
|
||||
`<span class="vorliebe-chip bw-${bw}">${escV(name)}</span>`).join('')}
|
||||
</div>
|
||||
</div>`).join('');
|
||||
} catch(e) {
|
||||
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Fehler beim Laden.</p>';
|
||||
}
|
||||
}
|
||||
function escV(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
function showPreviewBanner(mode) {
|
||||
const banner = document.createElement('div');
|
||||
banner.style.cssText = 'background:var(--color-secondary);border:1px solid var(--color-primary);border-radius:8px;padding:0.65rem 1rem;margin-bottom:1rem;font-size:0.88rem;display:flex;align-items:center;justify-content:space-between;gap:0.75rem;';
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
.aufgaben-section-label { font-size: 0.78rem; font-weight: 600; color: var(--color-muted); text-transform: uppercase; letter-spacing: 0.04em; margin: 1rem 0 0.5rem 0; }
|
||||
.aufgaben-section-label:first-child { margin-top: 0; }
|
||||
.toys-hint { font-size: 0.85rem; color: var(--color-muted); margin-bottom: 1rem; }
|
||||
.toys-badge { font-size: 0.75rem; font-weight: 600; background: var(--color-primary); color: #fff; border-radius: 999px; padding: 0.1em 0.55em; margin-left: 0.5rem; vertical-align: middle; }
|
||||
|
||||
/* ── Guest hint ── */
|
||||
.guest-hint { font-size: 0.8rem; color: var(--color-muted); font-style: italic; margin-bottom: 0.75rem; padding: 0.5rem 0.75rem; background: var(--color-secondary); border-radius: 6px; }
|
||||
@@ -210,7 +211,7 @@
|
||||
<!-- Accordion 4: Toys -->
|
||||
<div class="acc-item">
|
||||
<button class="acc-header" id="acc-toys-btn" onclick="toggleAcc('toys')">
|
||||
<span>Toys</span><span class="acc-chevron">▾</span>
|
||||
<span>Toys<span id="toys-badge" class="toys-badge" style="display:none;"></span></span><span class="acc-chevron">▾</span>
|
||||
</button>
|
||||
<div class="acc-body" id="acc-toys-body">
|
||||
<div id="guestToysHint" class="guest-hint" style="display:none;">Toys werden vom Host festgelegt – nur zur Ansicht.</div>
|
||||
@@ -536,6 +537,8 @@
|
||||
function onGruppenChanged() {
|
||||
warnungsAkzeptiert = false; hideMessage();
|
||||
gruppenContent = null; loadedForGruppen = null; toysNeedReload = true;
|
||||
const badge = document.getElementById('toys-badge');
|
||||
badge.style.display = 'none'; badge.textContent = '';
|
||||
if (document.getElementById('acc-toys-body').classList.contains('is-open')) ladeToys();
|
||||
}
|
||||
|
||||
@@ -605,23 +608,35 @@
|
||||
|
||||
async function ladeToys() {
|
||||
const toyList = document.getElementById('toyList');
|
||||
toyList.innerHTML = '<p class="empty-hint">Lade…</p>';
|
||||
const content = await ladeGruppenContent();
|
||||
const toyMap = new Map();
|
||||
[...content.aufgaben, ...content.finisher].forEach(item =>
|
||||
(item.benoetigteToys || []).forEach(t => { if (!toyMap.has(t.toyId)) toyMap.set(t.toyId, t); }));
|
||||
const toys = [...toyMap.values()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
if (!toys.length) {
|
||||
toyList.innerHTML = '<p class="empty-hint">Keine Toys erforderlich – alle Aufgaben können gespielt werden.</p>'; return;
|
||||
const selected = getSelectedGruppen();
|
||||
if (!selected.length) {
|
||||
toyList.innerHTML = '<p class="empty-hint">Bitte zuerst Aufgaben-Gruppen auswählen.</p>';
|
||||
toysNeedReload = false; return;
|
||||
}
|
||||
toyList.innerHTML = '<p class="empty-hint">Lade…</p>';
|
||||
try {
|
||||
const params = selected.map(id => `gruppenIds=${encodeURIComponent(id)}`).join('&');
|
||||
const res = await fetch(`/vanilla/toy/required?${params}`);
|
||||
if (!res.ok) throw new Error();
|
||||
const toys = await res.json();
|
||||
toysNeedReload = false;
|
||||
const badge = document.getElementById('toys-badge');
|
||||
if (toys.length) { badge.textContent = toys.length; badge.style.display = ''; }
|
||||
else { badge.style.display = 'none'; badge.textContent = ''; }
|
||||
if (!toys.length) {
|
||||
toyList.innerHTML = '<p class="empty-hint">Keine Toys erforderlich – alle Aufgaben können gespielt werden.</p>'; return;
|
||||
}
|
||||
toyList.innerHTML = toys.map(toy => {
|
||||
const checked = savedToyIds === null || savedToyIds.has(toy.toyId);
|
||||
return `<label class="toy-item${checked ? ' is-checked' : ''}">
|
||||
<input type="checkbox" value="${toy.toyId}"${checked ? ' checked' : ''}>
|
||||
<span><span class="toy-item-name">${toy.name}</span>${toy.beschreibung ? `<span class="toy-item-desc">${toy.beschreibung}</span>` : ''}</span>
|
||||
${toy.bild ? `<img class="item-img" src="data:image/png;base64,${toy.bild}" alt="">` : ''}
|
||||
</label>`;
|
||||
}).join('');
|
||||
} catch (_) {
|
||||
toyList.innerHTML = '<p class="empty-hint">Fehler beim Laden der Toys.</p>';
|
||||
}
|
||||
toyList.innerHTML = toys.map(toy => {
|
||||
const checked = savedToyIds === null || savedToyIds.has(toy.toyId);
|
||||
return `<label class="toy-item${checked ? ' is-checked' : ''}">
|
||||
<input type="checkbox" value="${toy.toyId}"${checked ? ' checked' : ''}>
|
||||
<span><span class="toy-item-name">${toy.name}</span>${toy.beschreibung ? `<span class="toy-item-desc">${toy.beschreibung}</span>` : ''}</span>
|
||||
${toy.bild ? `<img class="item-img" src="data:image/png;base64,${toy.bild}" alt="">` : ''}
|
||||
</label>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function validateContent(content, mitspieler) {
|
||||
|
||||
BIN
xxxthegame/src/main/resources/static/img/vorlieben/dunno.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
xxxthegame/src/main/resources/static/img/vorlieben/negative.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
xxxthegame/src/main/resources/static/img/vorlieben/neutral.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
xxxthegame/src/main/resources/static/img/vorlieben/positiv.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -566,6 +566,19 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Vorlieben -->
|
||||
<div class="settings-row">
|
||||
<div class="settings-row-info">
|
||||
<div class="settings-row-label">Vorlieben</div>
|
||||
<div class="settings-row-desc">Deine Vorlieben und Bewertungen im Profil</div>
|
||||
</div>
|
||||
<select id="sv-vorlieben" onchange="doSave()">
|
||||
<option value="ALLE">Alle</option>
|
||||
<option value="NUR_FREUNDE">Nur Freunde</option>
|
||||
<option value="NUR_ICH">Nur ich</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Profil bei Veröffentlichungen -->
|
||||
<div class="settings-row">
|
||||
<div class="settings-row-info">
|
||||
@@ -974,6 +987,7 @@
|
||||
setValue('sv-pinnwand', profile.sichtbarkeitPinnwand || 'ALLE');
|
||||
setValue('sv-xp', profile.sichtbarkeitXp || 'ALLE');
|
||||
setValue('sv-lockhistorie', profile.sichtbarkeitLockhistorie || 'ALLE');
|
||||
setValue('sv-vorlieben', profile.sichtbarkeitVorlieben || 'ALLE');
|
||||
setValue('sv-veroeffentlichungen', profile.profilBeiVeroeffentlichungenSichtbar ? 'true' : 'false');
|
||||
}
|
||||
|
||||
@@ -991,6 +1005,7 @@
|
||||
sichtbarkeitPinnwand: document.getElementById('sv-pinnwand').value,
|
||||
sichtbarkeitXp: document.getElementById('sv-xp').value,
|
||||
sichtbarkeitLockhistorie: document.getElementById('sv-lockhistorie').value,
|
||||
sichtbarkeitVorlieben: document.getElementById('sv-vorlieben').value,
|
||||
profilBeiVeroeffentlichungenSichtbar: document.getElementById('sv-veroeffentlichungen').value === 'true',
|
||||
};
|
||||
const res = await fetch('/user/me/privacy', {
|
||||
|
||||
@@ -95,6 +95,39 @@
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ── Vorlieben Tabs ── */
|
||||
.vl-tabs { display: flex; gap: 0; flex-wrap: wrap;
|
||||
border-bottom: 1px solid var(--color-secondary); margin-bottom: 1rem; }
|
||||
.vl-tab-btn { background: none; border: none; border-bottom: 3px solid transparent;
|
||||
border-radius: 0; padding: 0.5rem 1rem; font-size: 0.85rem; font-weight: 600;
|
||||
color: var(--color-muted); cursor: pointer; margin-bottom: -1px;
|
||||
transition: color .15s, border-color .15s; white-space: nowrap; }
|
||||
.vl-tab-btn:hover { color: var(--color-text); background: none; }
|
||||
.vl-tab-btn.active { color: var(--color-primary); border-bottom-color: var(--color-primary); }
|
||||
.vl-tab-panel { display: none; }
|
||||
.vl-tab-panel.active { display: block; }
|
||||
|
||||
/* ── Vorlieben Items ── */
|
||||
.vorliebe-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 0.75rem; padding: 0.55rem 0.75rem; border-radius: 8px;
|
||||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
.vorliebe-row-name { font-size: 0.9rem; flex: 1; min-width: 0; }
|
||||
.vorliebe-smileys { display: flex; gap: 0.25rem; flex-shrink: 0; }
|
||||
.vl-smiley {
|
||||
display: inline-flex; align-items: center; cursor: pointer;
|
||||
border: 2px solid transparent; border-radius: 6px;
|
||||
padding: 0.1rem 0.15rem;
|
||||
transition: border-color .15s, transform .1s;
|
||||
user-select: none;
|
||||
}
|
||||
.vl-smiley img { height: 1.6rem; width: auto; display: block; }
|
||||
.vl-smiley:hover { transform: scale(1.15); }
|
||||
.vl-smiley.active { border-color: var(--vl-color); }
|
||||
.vorlieben-hint { font-size: 0.82rem; color: var(--color-muted); margin-bottom: 1rem; }
|
||||
|
||||
#ownGallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
@@ -334,6 +367,12 @@
|
||||
|
||||
<button class="full-width" id="saveBtn" onclick="saveProfile()">Profil speichern</button>
|
||||
|
||||
<div class="gallery-section-label" style="margin-top:1.5rem;">Vorlieben</div>
|
||||
<p class="vorlieben-hint">Wähle für jede Vorliebe aus, wie du dazu stehst. Nicht ausgefüllte Einträge werden nicht angezeigt.</p>
|
||||
<div id="vorliebenSection"><p style="color:var(--color-muted);font-size:0.85rem;">Wird geladen…</p></div>
|
||||
<div class="message" id="vorliebenMessage" style="margin-top:0.75rem;display:none;"></div>
|
||||
<button class="full-width" id="saveVorliebenBtn" onclick="saveVorlieben()" style="margin-bottom:1.5rem;">Vorlieben speichern</button>
|
||||
|
||||
<div class="gallery-section-label">Meine Bilder</div>
|
||||
<div class="gallery-upload-row">
|
||||
<input type="file" id="galleryFile" accept="image/*" multiple style="display:none;" onchange="handleGalleryUpload(this.files)">
|
||||
@@ -384,6 +423,7 @@
|
||||
}
|
||||
myUserId = user.userId;
|
||||
loadOwnGallery();
|
||||
loadVorliebenEdit();
|
||||
})
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
|
||||
@@ -596,6 +636,131 @@
|
||||
function hideMessage() {
|
||||
document.getElementById('message').style.display = 'none';
|
||||
}
|
||||
|
||||
// ── Vorlieben ──────────────────────────────────────────────────────────────
|
||||
|
||||
const VL_SMILEYS = [
|
||||
{ value: 'GEHT_GAR_NICHT', img: '/img/vorlieben/verynegative.png', color: '#e53935', title: 'Geht gar nicht' },
|
||||
{ value: 'EHER_NICHT', img: '/img/vorlieben/negative.png', color: '#fb8c00', title: 'Eher nicht' },
|
||||
{ value: 'NEUTRAL', img: '/img/vorlieben/neutral.png', color: '#fdd835', title: 'Neutral' },
|
||||
{ value: 'MAG_ICH', img: '/img/vorlieben/positiv.png', color: '#81c784', title: 'Mag ich' },
|
||||
{ value: 'UNBEDINGT', img: '/img/vorlieben/verypositiv.png', color: '#2e7d32', title: 'Unbedingt' },
|
||||
{ value: 'WILL_AUSPROBIEREN', img: '/img/vorlieben/dunno.png', color: '#1e88e5', title: 'Will ich ausprobieren' },
|
||||
];
|
||||
|
||||
let vorliebenRatings = {};
|
||||
|
||||
async function loadVorliebenEdit() {
|
||||
const container = document.getElementById('vorliebenSection');
|
||||
try {
|
||||
const [itemsRes, meRes] = await Promise.all([
|
||||
fetch('/vorlieben/items'),
|
||||
fetch('/vorlieben/me'),
|
||||
]);
|
||||
if (!itemsRes.ok) {
|
||||
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Keine Vorlieben konfiguriert.</p>';
|
||||
return;
|
||||
}
|
||||
const kategorien = await itemsRes.json();
|
||||
vorliebenRatings = meRes.ok ? await meRes.json() : {};
|
||||
|
||||
if (!kategorien.length) {
|
||||
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Keine Vorlieben konfiguriert.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const smileysHtml = VL_SMILEYS.map(s =>
|
||||
`<span class="vl-smiley" data-val="${s.value}" title="${s.title}" style="--vl-color:${s.color}"><img src="${s.img}" alt="${s.title}"></span>`
|
||||
).join('');
|
||||
|
||||
// Tab buttons
|
||||
const tabBtns = kategorien.map((kat, i) =>
|
||||
`<button class="vl-tab-btn${i === 0 ? ' active' : ''}" onclick="switchVlTab('vlkat-${kat.kategorieId}', this)">${escapeHtml(kat.name)}</button>`
|
||||
).join('');
|
||||
|
||||
// Tab panels
|
||||
const tabPanels = kategorien.map((kat, i) => `
|
||||
<div class="vl-tab-panel${i === 0 ? ' active' : ''}" id="vlkat-${kat.kategorieId}">
|
||||
${kat.items.map(item => `
|
||||
<div class="vorliebe-row">
|
||||
<span class="vorliebe-row-name">${escapeHtml(item.name)}</span>
|
||||
<div class="vorliebe-smileys" data-item-id="${item.itemId}">
|
||||
${smileysHtml}
|
||||
</div>
|
||||
</div>`).join('')}
|
||||
</div>`).join('');
|
||||
|
||||
container.innerHTML = `<div class="vl-tabs">${tabBtns}</div>${tabPanels}`;
|
||||
|
||||
// Mark saved values
|
||||
container.querySelectorAll('.vorliebe-smileys[data-item-id]').forEach(group => {
|
||||
const saved = vorliebenRatings[group.dataset.itemId];
|
||||
if (saved) {
|
||||
const btn = group.querySelector(`.vl-smiley[data-val="${saved}"]`);
|
||||
if (btn) btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Click handler
|
||||
container.addEventListener('click', e => {
|
||||
const btn = e.target.closest('.vl-smiley');
|
||||
if (!btn) return;
|
||||
const group = btn.closest('.vorliebe-smileys');
|
||||
const itemId = group.dataset.itemId;
|
||||
const isActive = btn.classList.contains('active');
|
||||
// Deselect all in this group
|
||||
group.querySelectorAll('.vl-smiley').forEach(s => s.classList.remove('active'));
|
||||
if (!isActive) {
|
||||
btn.classList.add('active');
|
||||
vorliebenRatings[itemId] = btn.dataset.val;
|
||||
} else {
|
||||
delete vorliebenRatings[itemId];
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Fehler beim Laden.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function switchVlTab(panelId, btn) {
|
||||
document.querySelectorAll('.vl-tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.vl-tab-panel').forEach(p => p.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById(panelId).classList.add('active');
|
||||
}
|
||||
|
||||
async function saveVorlieben() {
|
||||
const btn = document.getElementById('saveVorliebenBtn');
|
||||
const msgEl = document.getElementById('vorliebenMessage');
|
||||
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
|
||||
|
||||
// Collect: all known items → current rating or null
|
||||
const ratings = {};
|
||||
document.querySelectorAll('#vorliebenSection .vorliebe-smileys[data-item-id]').forEach(group => {
|
||||
const active = group.querySelector('.vl-smiley.active');
|
||||
ratings[group.dataset.itemId] = active ? active.dataset.val : null;
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch('/vorlieben/me', {
|
||||
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ratings),
|
||||
});
|
||||
msgEl.textContent = res.ok ? 'Vorlieben gespeichert.' : 'Fehler beim Speichern.';
|
||||
msgEl.className = `message ${res.ok ? 'success' : 'error'}`;
|
||||
msgEl.style.display = 'block';
|
||||
setTimeout(() => { msgEl.style.display = 'none'; }, 3000);
|
||||
} catch (e) {
|
||||
msgEl.textContent = 'Fehler beim Speichern.'; msgEl.className = 'message error'; msgEl.style.display = 'block';
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = 'Vorlieben speichern';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||