From 878ede914b69165fae906cfc18bf2279807eb1e3 Mon Sep 17 00:00:00 2001 From: lobo Date: Thu, 1 Oct 2026 15:09:25 +0200 Subject: [PATCH] feat(settings): reduce animations appearance flag --- readme.md | 4 ++-- .../user/api/dto/request/AppearanceRequest.java | 3 ++- .../user/api/dto/response/AppearanceResponse.java | 2 +- .../modules/user/api/mapper/ApiMapper.java | 6 ++++-- .../auth_service/modules/user/model/Appearance.java | 5 +++-- .../user/persistence/entities/UserSettingsEntity.java | 3 +++ .../modules/user/persistence/mapper/EntityMapper.java | 4 +++- .../resources/db/migration/V26__Reduce_Motion.sql | 3 +++ .../modules/user/api/UserControllerTest.java | 11 ++++++----- .../modules/user/persistence/UserRepositoryTest.java | 4 ++-- .../modules/user/service/UserServiceTest.java | 2 +- 11 files changed, 30 insertions(+), 17 deletions(-) create mode 100644 src/main/resources/db/migration/V26__Reduce_Motion.sql diff --git a/readme.md b/readme.md index 34b8171..5b1da23 100644 --- a/readme.md +++ b/readme.md @@ -33,8 +33,8 @@ To Be Played is split into three repositories: as verified. A one-time state plus a binding cookie tie the answer to the browser that started the link, so a foreign Steam login link cannot move someone's Steam ID into another account. IDs typed in before this stay, but unverified: a verified link from another account takes them away. Unlinking is possible. -- Appearance settings synced across devices: accent color, corner style, wide layout, background and dashboard - banner from the user's own game art. Only whitelisted presets and Steam app ids are stored, never raw CSS or URLs. +- Appearance settings synced across devices: accent color, corner style, wide layout, reduce animations, background and + dashboard banner from the user's own game art. Only whitelisted presets and Steam app ids are stored, never raw CSS or URLs. - Opt-in public profile at `/u/`. Auth answers with a short-lived **profile ticket** (EdDSA JWT, 10 minutes, `typ=public-profile`) that Core accepts for the public library data only. The profile has its own look (accent, corners, banner and background from own game art) and a **tile layout** (`profile_tiles`: the shown diff --git a/src/main/java/auth_service/modules/user/api/dto/request/AppearanceRequest.java b/src/main/java/auth_service/modules/user/api/dto/request/AppearanceRequest.java index 65a1a35..55a5855 100644 --- a/src/main/java/auth_service/modules/user/api/dto/request/AppearanceRequest.java +++ b/src/main/java/auth_service/modules/user/api/dto/request/AppearanceRequest.java @@ -13,7 +13,8 @@ public record AppearanceRequest( Boolean wideLayout, @Positive Long backgroundAppid, @Positive Long bannerAppid, - Boolean showBanner) { + Boolean showBanner, + Boolean reduceMotion) { public static final String ACCENTS = "violet|blue|green|orange|pink|red"; public static final String CORNERS = "sharp|default|round"; diff --git a/src/main/java/auth_service/modules/user/api/dto/response/AppearanceResponse.java b/src/main/java/auth_service/modules/user/api/dto/response/AppearanceResponse.java index 93d53de..6eb5f77 100644 --- a/src/main/java/auth_service/modules/user/api/dto/response/AppearanceResponse.java +++ b/src/main/java/auth_service/modules/user/api/dto/response/AppearanceResponse.java @@ -1,5 +1,5 @@ package auth_service.modules.user.api.dto.response; public record AppearanceResponse(String accent, String corners, boolean wideLayout, Long backgroundAppid, - Long bannerAppid, boolean showBanner) { + Long bannerAppid, boolean showBanner, boolean reduceMotion) { } diff --git a/src/main/java/auth_service/modules/user/api/mapper/ApiMapper.java b/src/main/java/auth_service/modules/user/api/mapper/ApiMapper.java index de6ccb0..0c0742e 100644 --- a/src/main/java/auth_service/modules/user/api/mapper/ApiMapper.java +++ b/src/main/java/auth_service/modules/user/api/mapper/ApiMapper.java @@ -56,7 +56,8 @@ public abstract class ApiMapper { private static AppearanceResponse toApi(Appearance appearance) { return new AppearanceResponse(appearance.accent(), appearance.corners(), appearance.wideLayout(), - appearance.backgroundAppid(), appearance.bannerAppid(), appearance.showBanner()); + appearance.backgroundAppid(), appearance.bannerAppid(), appearance.showBanner(), + appearance.reduceMotion()); } private static ProfileStyleResponse toApi(ProfileStyle style) { @@ -81,7 +82,8 @@ public abstract class ApiMapper { public static Appearance toDomain(AppearanceRequest request) { // A missing showBanner keeps the banner on, the default. return new Appearance(request.accent(), request.corners(), Boolean.TRUE.equals(request.wideLayout()), - request.backgroundAppid(), request.bannerAppid(), !Boolean.FALSE.equals(request.showBanner())); + request.backgroundAppid(), request.bannerAppid(), !Boolean.FALSE.equals(request.showBanner()), + Boolean.TRUE.equals(request.reduceMotion())); } public static UserSettings toDomain(UserSettingsRequest request) { diff --git a/src/main/java/auth_service/modules/user/model/Appearance.java b/src/main/java/auth_service/modules/user/model/Appearance.java index ca3bde9..873700b 100644 --- a/src/main/java/auth_service/modules/user/model/Appearance.java +++ b/src/main/java/auth_service/modules/user/model/Appearance.java @@ -4,9 +4,10 @@ package auth_service.modules.user.model; * @param backgroundAppid the Steam app whose art fills the page background; null for none * @param bannerAppid the Steam app whose art is the dashboard banner; null for the most played game of the last * two weeks + * @param reduceMotion viewer setting: every animated look is static, also on other users' profiles */ public record Appearance(String accent, String corners, boolean wideLayout, Long backgroundAppid, - Long bannerAppid, boolean showBanner) { + Long bannerAppid, boolean showBanner, boolean reduceMotion) { - public static final Appearance DEFAULT = new Appearance("violet", "default", false, null, null, true); + public static final Appearance DEFAULT = new Appearance("violet", "default", false, null, null, true, false); } diff --git a/src/main/java/auth_service/modules/user/persistence/entities/UserSettingsEntity.java b/src/main/java/auth_service/modules/user/persistence/entities/UserSettingsEntity.java index aa98d41..be969c4 100644 --- a/src/main/java/auth_service/modules/user/persistence/entities/UserSettingsEntity.java +++ b/src/main/java/auth_service/modules/user/persistence/entities/UserSettingsEntity.java @@ -50,6 +50,9 @@ public class UserSettingsEntity { @Column(name = "show_banner", nullable = false) private boolean showBanner; + @Column(name = "reduce_motion", nullable = false) + private boolean reduceMotion; + @Column(name = "profile_accent", nullable = false) private String profileAccent; diff --git a/src/main/java/auth_service/modules/user/persistence/mapper/EntityMapper.java b/src/main/java/auth_service/modules/user/persistence/mapper/EntityMapper.java index 535f380..90fa861 100644 --- a/src/main/java/auth_service/modules/user/persistence/mapper/EntityMapper.java +++ b/src/main/java/auth_service/modules/user/persistence/mapper/EntityMapper.java @@ -28,7 +28,8 @@ public abstract class EntityMapper { .theme(settings.getTheme()) .publicProfile(settings.isPublicProfile()) .appearance(new Appearance(settings.getAccent(), settings.getCorners(), settings.isWideLayout(), - settings.getBackgroundAppid(), settings.getBannerAppid(), settings.isShowBanner())) + settings.getBackgroundAppid(), settings.getBannerAppid(), settings.isShowBanner(), + settings.isReduceMotion())) .profileStyle(new ProfileStyle(settings.getProfileAccent(), settings.getProfileCorners(), settings.getProfileBannerAppid(), settings.getProfileBackgroundAppid())) .dashboardTiles(settings.getDashboardTiles() == null ? null @@ -66,6 +67,7 @@ public abstract class EntityMapper { target.setBackgroundAppid(appearance.backgroundAppid()); target.setBannerAppid(appearance.bannerAppid()); target.setShowBanner(appearance.showBanner()); + target.setReduceMotion(appearance.reduceMotion()); ProfileStyle style = settings.getProfileStyle(); target.setProfileAccent(style.accent()); target.setProfileCorners(style.corners()); diff --git a/src/main/resources/db/migration/V26__Reduce_Motion.sql b/src/main/resources/db/migration/V26__Reduce_Motion.sql new file mode 100644 index 0000000..c65bbbc --- /dev/null +++ b/src/main/resources/db/migration/V26__Reduce_Motion.sql @@ -0,0 +1,3 @@ +-- Viewer setting: every animated look is static, also on other users' profiles. +ALTER TABLE user_settings + ADD COLUMN reduce_motion BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/src/test/java/auth_service/modules/user/api/UserControllerTest.java b/src/test/java/auth_service/modules/user/api/UserControllerTest.java index 06fd8d6..bbbcd2b 100644 --- a/src/test/java/auth_service/modules/user/api/UserControllerTest.java +++ b/src/test/java/auth_service/modules/user/api/UserControllerTest.java @@ -328,7 +328,7 @@ class UserControllerTest extends AbstractWebMvcTest { void getCurrentUser_ShowsAppearance() throws Exception { var email = "test@example.com"; var settings = UserSettings.builder().language("en").theme("dark") - .appearance(new Appearance("blue", "round", true, 620L, 1086940L, false)).build(); + .appearance(new Appearance("blue", "round", true, 620L, 1086940L, false, true)).build(); SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(new UserPrincipal("1", null), null, Collections.emptyList()) ); @@ -341,7 +341,8 @@ class UserControllerTest extends AbstractWebMvcTest { .andExpect(jsonPath("$.settings.appearance.wideLayout").value(true)) .andExpect(jsonPath("$.settings.appearance.backgroundAppid").value(620)) .andExpect(jsonPath("$.settings.appearance.bannerAppid").value(1086940)) - .andExpect(jsonPath("$.settings.appearance.showBanner").value(false)); + .andExpect(jsonPath("$.settings.appearance.showBanner").value(false)) + .andExpect(jsonPath("$.settings.appearance.reduceMotion").value(true)); } @Test @@ -629,10 +630,10 @@ class UserControllerTest extends AbstractWebMvcTest { mockMvc.perform(put("/api/users/settings/appearance") .contentType(MediaType.APPLICATION_JSON) .content("{\"accent\":\"green\",\"corners\":\"sharp\",\"wideLayout\":true,\"backgroundAppid\":620," - + "\"bannerAppid\":1086940,\"showBanner\":false}")) + + "\"bannerAppid\":1086940,\"showBanner\":false,\"reduceMotion\":true}")) .andExpect(status().isNoContent()); - verify(userService).updateAppearance(1L, new Appearance("green", "sharp", true, 620L, 1086940L, false)); + verify(userService).updateAppearance(1L, new Appearance("green", "sharp", true, 620L, 1086940L, false, true)); } @Test @@ -645,7 +646,7 @@ class UserControllerTest extends AbstractWebMvcTest { .content("{\"accent\":\"violet\",\"corners\":\"default\"}")) .andExpect(status().isNoContent()); - verify(userService).updateAppearance(1L, new Appearance("violet", "default", false, null, null, true)); + verify(userService).updateAppearance(1L, new Appearance("violet", "default", false, null, null, true, false)); } // Each body differs from a valid one in one field only, so the 400 comes from that field's constraint. diff --git a/src/test/java/auth_service/modules/user/persistence/UserRepositoryTest.java b/src/test/java/auth_service/modules/user/persistence/UserRepositoryTest.java index 2c04270..84b6fc7 100644 --- a/src/test/java/auth_service/modules/user/persistence/UserRepositoryTest.java +++ b/src/test/java/auth_service/modules/user/persistence/UserRepositoryTest.java @@ -276,7 +276,7 @@ class UserRepositoryTest extends AbstractPersistenceTest { .registrationDate(LocalDateTime.now()) .build()); userRepository.update(userEntity.getId(), - user -> user.getSettings().setAppearance(new Appearance("pink", "sharp", false, 220L, null, true))); + user -> user.getSettings().setAppearance(new Appearance("pink", "sharp", false, 220L, null, true, true))); entityManager.flush(); updateSettings(userEntity.getId(), null, @@ -285,7 +285,7 @@ class UserRepositoryTest extends AbstractPersistenceTest { entityManager.clear(); var settings = findByEmail("keep@example.com").orElseThrow().getSettings(); - assertEquals(new Appearance("pink", "sharp", false, 220L, null, true), settings.getAppearance()); + assertEquals(new Appearance("pink", "sharp", false, 220L, null, true, true), settings.getAppearance()); } @Test diff --git a/src/test/java/auth_service/modules/user/service/UserServiceTest.java b/src/test/java/auth_service/modules/user/service/UserServiceTest.java index 36facc8..1cbfe0c 100644 --- a/src/test/java/auth_service/modules/user/service/UserServiceTest.java +++ b/src/test/java/auth_service/modules/user/service/UserServiceTest.java @@ -180,7 +180,7 @@ class UserServiceTest extends AbstractUnitTest { @Test void updateAppearance_StoresItWithoutANewToken() { - var appearance = new Appearance("red", "round", false, null, null, true); + var appearance = new Appearance("red", "round", false, null, null, true, false); userService.updateAppearance(1L, appearance);