From 763bd6305608f8c1e432c2da887c28b5b9bd63fe Mon Sep 17 00:00:00 2001 From: Simon Grimme <9295182+grimsi@users.noreply.github.com> Date: Fri, 12 Aug 2022 22:08:19 +0200 Subject: [PATCH 01/16] Added support for multiple library folders --- .../gameyfin/service/LibraryService.java | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java index 3531176..49876e7 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java +++ b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java @@ -10,14 +10,17 @@ import de.grimsi.gameyfin.repositories.DetectedGameRepository; import de.grimsi.gameyfin.repositories.UnmappableFileRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.StopWatch; import java.io.IOException; -import java.nio.file.*; -import java.util.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; @@ -30,24 +33,28 @@ import static de.grimsi.gameyfin.util.FilenameUtil.hasGameArchiveExtension; public class LibraryService { @Value("${gameyfin.root}") - private String rootFolderPath; + private List libraryFolders; private final IgdbWrapper igdbWrapper; private final DetectedGameRepository detectedGameRepository; private final UnmappableFileRepository unmappableFileRepository; public List getGameFiles() { + List gamefiles = new ArrayList<>(); - Path rootFolder = Path.of(rootFolderPath); + libraryFolders.parallelStream().map(Path::of).forEach(folder -> { + try (Stream stream = Files.list(folder)) { + // return all sub-folders (non-recursive) and files that have an extension that indicates that they are a downloadable file + List gameFilesFromThisFolder = stream.filter(p -> Files.isDirectory(p) || hasGameArchiveExtension(p)).toList(); + gamefiles.addAll(gameFilesFromThisFolder); - try (Stream stream = Files.list(rootFolder)) { - // return all sub-folders (non-recursive) and files that have an extension that indicates that they are a downloadable file - return stream - .filter(p -> Files.isDirectory(p) || hasGameArchiveExtension(p)) - .toList(); - } catch (IOException e) { - throw new RuntimeException("Error while opening root folder", e); - } + } catch (IOException e) { + throw new RuntimeException("Error while opening library folder '%s'".formatted(folder), e); + } + } + ); + + return gamefiles; } public void scanGameLibrary() { From 63d585b5d6191e32e9ed903a6a32137e155daafc Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Fri, 12 Aug 2022 23:36:47 +0200 Subject: [PATCH 02/16] Fixed some bugs related to the property files --- .gitignore | 3 +- backend/pom.xml | 6 ++++ .../grimsi/gameyfin/GameyfinApplication.java | 2 +- .../gameyfin/config/SecureProperties.java | 2 +- .../gameyfin/service/LibraryService.java | 8 ++---- ...itional-spring-configuration-metadata.json | 9 ++++++ .../src/main/resources/application-dev.yml | 2 -- .../main/resources/config/database.properties | 13 --------- .../src/main/resources/config/database.yml | 18 ++++++++++++ .../main/resources/config/gameyfin.properties | 28 ------------------- .../src/main/resources/config/gameyfin.yml | 19 +++++++++++++ .../main/resources/config/secure.properties | 19 ------------- backend/src/main/resources/config/secure.yml | 27 ++++++++++++++++++ docker/docker-compose.example.yml | 4 +++ 14 files changed, 90 insertions(+), 70 deletions(-) create mode 100644 backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json delete mode 100644 backend/src/main/resources/config/database.properties create mode 100644 backend/src/main/resources/config/database.yml delete mode 100644 backend/src/main/resources/config/gameyfin.properties create mode 100644 backend/src/main/resources/config/gameyfin.yml delete mode 100644 backend/src/main/resources/config/secure.properties create mode 100644 backend/src/main/resources/config/secure.yml diff --git a/.gitignore b/.gitignore index 5d839f3..bbaa093 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ build/ ### Custom ### /data/ /backend/src/main/resources/static/ -/docker/docker-compose.yml \ No newline at end of file +/docker/docker-compose.yml +/.gameyfin/ diff --git a/backend/pom.xml b/backend/pom.xml index 9734656..6906b6d 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -16,12 +16,16 @@ 18 + 1.6.9 1.7.1 2.11.0 1.21 3.11.4 3.21.3 + + + 3.1.0 @@ -140,6 +144,7 @@ **/*.properties **/*.yml **/*.yaml + **/*.json **/*.txt **/*.js **/*.css @@ -166,6 +171,7 @@ maven-resources-plugin + ${maven-resources-plugin.version} copy-resources diff --git a/backend/src/main/java/de/grimsi/gameyfin/GameyfinApplication.java b/backend/src/main/java/de/grimsi/gameyfin/GameyfinApplication.java index 4c79e1b..f17d349 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/GameyfinApplication.java +++ b/backend/src/main/java/de/grimsi/gameyfin/GameyfinApplication.java @@ -8,7 +8,7 @@ public class GameyfinApplication { public static void main(String[] args) { new SpringApplicationBuilder(GameyfinApplication.class) - .properties("spring.config.name=application,gameyfin,database,secure") + .properties( "file.encoding=UTF-8", "spring.config.name=application,gameyfin,database,secure") .build() .run(args); } diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java b/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java index 6d66527..3140b37 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java +++ b/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java @@ -16,7 +16,7 @@ public class SecureProperties { @Autowired public void setConfigurableEnvironment(ConfigurableEnvironment env) { try { - Resource resource = new ClassPathResource("/config/secure.properties"); + Resource resource = new ClassPathResource("/config/secure.yml"); env.getPropertySources().addFirst(new PropertiesPropertySource(Objects.requireNonNull(resource.getFilename()), PropertiesLoaderUtils.loadProperties(resource))); } catch (Exception ex) { throw new RuntimeException(ex.getMessage(), ex); diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java index 49876e7..0f1770c 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java +++ b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java @@ -17,10 +17,7 @@ import org.springframework.util.StopWatch; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; @@ -42,7 +39,8 @@ public class LibraryService { public List getGameFiles() { List gamefiles = new ArrayList<>(); - libraryFolders.parallelStream().map(Path::of).forEach(folder -> { + libraryFolders.parallelStream().map(Path::of).forEach( + folder -> { try (Stream stream = Files.list(folder)) { // return all sub-folders (non-recursive) and files that have an extension that indicates that they are a downloadable file List gameFilesFromThisFolder = stream.filter(p -> Files.isDirectory(p) || hasGameArchiveExtension(p)).toList(); diff --git a/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..41bd021 --- /dev/null +++ b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "properties": [ + { + "name": "gameyfin.root", + "type": "java.lang.String[]", + "description": "List of directories Gameyfin should scan for games." + } + ] +} \ No newline at end of file diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index 30efaba..37e7822 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -1,8 +1,6 @@ gameyfin: user: admin password: password - cache: ${gameyfin.root}\.gameyfin\cache - db: ${gameyfin.root}\.gameyfin\db logging: level: diff --git a/backend/src/main/resources/config/database.properties b/backend/src/main/resources/config/database.properties deleted file mode 100644 index 6123b15..0000000 --- a/backend/src/main/resources/config/database.properties +++ /dev/null @@ -1,13 +0,0 @@ -# This file contains properties related to the database configuration -# -# -spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true -spring.datasource.db-name=gameyfin_db -spring.datasource.url=jdbc:h2:file:${gameyfin.db}/${spring.datasource.db-name} -spring.datasource.username=gfadmin -spring.datasource.password=gameyfin -spring.datasource.driverClassName=org.h2.Driver -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.hibernate.ddl-auto=update -spring.jpa.open-in-view=true -spring.jpa.properties.hibernate.event.merge.entity_copy_observer=allow \ No newline at end of file diff --git a/backend/src/main/resources/config/database.yml b/backend/src/main/resources/config/database.yml new file mode 100644 index 0000000..7c473a2 --- /dev/null +++ b/backend/src/main/resources/config/database.yml @@ -0,0 +1,18 @@ +spring: + jpa: + open-in-view: 'true' + properties: + hibernate: + enable_lazy_load_no_trans: 'true' + event: + merge: + entity_copy_observer: allow + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: update + datasource: + username: gfadmin + url: jdbc:h2:file:${gameyfin.db}/${spring.datasource.db-name} + driverClassName: org.h2.Driver + db-name: gameyfin_db + password: gameyfin \ No newline at end of file diff --git a/backend/src/main/resources/config/gameyfin.properties b/backend/src/main/resources/config/gameyfin.properties deleted file mode 100644 index 776836d..0000000 --- a/backend/src/main/resources/config/gameyfin.properties +++ /dev/null @@ -1,28 +0,0 @@ -# This file contains properties related to the configuration of Gameyfin -# -# -# Username and password for the web interface -gameyfin.user= -gameyfin.password= - -# Root folder of your game library -gameyfin.root= -# Folders where gameyfin will store cached images and the database -gameyfin.cache=${gameyfin.root}/.gameyfin/cache -gameyfin.db=${gameyfin.root}/.gameyfin/db - -# File extensions which gameyfin will recognize as game files -gameyfin.file-extensions=iso, zip, rar, 7z, exe - -# List of IGDB platform enums to limit search results. For possible values see: https://api-docs.igdb.com/#platform -gameyfin.igdb.config.preferred-platforms=6 - -# Twitch Client ID and Client Secret -gameyfin.igdb.api.client-id= -gameyfin.igdb.api.client-secret= - -# The IGDB API has a rate limit of 4 req/s -gameyfin.igdb.api.max-requests-per-second=4 - -# According to the docs, there is a maximum of 8 concurrent requests, but in my tests the actual limit was 4 and even then it sometimes failed, so I set it to 2 to be sure -gameyfin.igdb.api.max-concurrent-requests=2 \ No newline at end of file diff --git a/backend/src/main/resources/config/gameyfin.yml b/backend/src/main/resources/config/gameyfin.yml new file mode 100644 index 0000000..ec97065 --- /dev/null +++ b/backend/src/main/resources/config/gameyfin.yml @@ -0,0 +1,19 @@ +gameyfin: + user: + password: + + root: //NAS-Simon/Öffentlich/Spiele, C:/gameyfin-library + + db: ./.gameyfin/db + cache: ./.gameyfin/cache + + file-extensions: iso, zip, rar, 7z, exe + + igdb: + api: + client-id: + client-secret: + max-concurrent-requests: 2 + max-requests-per-second: 4 + config: + preferred-platforms: 6 \ No newline at end of file diff --git a/backend/src/main/resources/config/secure.properties b/backend/src/main/resources/config/secure.properties deleted file mode 100644 index cda7c4e..0000000 --- a/backend/src/main/resources/config/secure.properties +++ /dev/null @@ -1,19 +0,0 @@ -# This file contains properties that are *NOT* safe to override by the user -# In theory a user should not be able to override them since they will be loaded from the classpath at launch, overriding existing user properties with the same key -# -# -# System Info -application.name=Gameyfin -application.version=@project.version@ -# API -server.servlet.context-path=/ -# Spring Actuator -management.endpoints.enabled-by-default=false -management.endpoint.health.enabled=true -# Server -server.error.include-stacktrace=never -spring.mvc.async.request-timeout=-1 -# Jackson JSON Mapping -spring.jackson.default-property-inclusion=non_null -spring.jackson.mapper.accept-case-insensitive-enums=true -spring.jackson.deserialization.fail-on-unknown-properties=false diff --git a/backend/src/main/resources/config/secure.yml b/backend/src/main/resources/config/secure.yml new file mode 100644 index 0000000..cf80710 --- /dev/null +++ b/backend/src/main/resources/config/secure.yml @@ -0,0 +1,27 @@ +application: + version: '@project.version@' + name: Gameyfin + +server: + servlet: + context-path: / + error: + include-stacktrace: never + +spring: + jackson: + deserialization: + fail-on-unknown-properties: false + default-property-inclusion: non_null + mapper: + accept-case-insensitive-enums: true + mvc: + async: + request-timeout: -1 + +management: + endpoint: + health: + enabled: true + endpoints: + enabled-by-default: false \ No newline at end of file diff --git a/docker/docker-compose.example.yml b/docker/docker-compose.example.yml index 265152c..5c2e439 100644 --- a/docker/docker-compose.example.yml +++ b/docker/docker-compose.example.yml @@ -8,6 +8,10 @@ services: - gameyfin.password= - gameyfin.igdb.api.client-id= - gameyfin.igdb.api.client-secret= + # The following two environment variables only need to be set if you have more than one library folder. + # If you have just one you can safely delete them. + - gameyfin.cache=/.gameyfin/cache + - gameyfin.db=/.gameyfin/db volumes: - :/opt/gameyfin-library ports: From da8e075cfc8be424ff0ef6cc3de055064dd366aa Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Fri, 12 Aug 2022 23:37:47 +0200 Subject: [PATCH 03/16] Removed default value from gameyfin.yml --- backend/src/main/resources/config/gameyfin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/config/gameyfin.yml b/backend/src/main/resources/config/gameyfin.yml index ec97065..68089f9 100644 --- a/backend/src/main/resources/config/gameyfin.yml +++ b/backend/src/main/resources/config/gameyfin.yml @@ -2,7 +2,7 @@ gameyfin: user: password: - root: //NAS-Simon/Öffentlich/Spiele, C:/gameyfin-library + root: db: ./.gameyfin/db cache: ./.gameyfin/cache From 7a3a323212a2bf18073b8b34f8b66c3381552c4f Mon Sep 17 00:00:00 2001 From: Simon Grimme <9295182+grimsi@users.noreply.github.com> Date: Sat, 13 Aug 2022 11:19:36 +0200 Subject: [PATCH 04/16] [untested] Set db and cache path from first library root --- ...cureProperties.java => CustomConfiguratioLoader.java} | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) rename backend/src/main/java/de/grimsi/gameyfin/config/{SecureProperties.java => CustomConfiguratioLoader.java} (64%) diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java b/backend/src/main/java/de/grimsi/gameyfin/config/CustomConfiguratioLoader.java similarity index 64% rename from backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java rename to backend/src/main/java/de/grimsi/gameyfin/config/CustomConfiguratioLoader.java index 3140b37..467435f 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java +++ b/backend/src/main/java/de/grimsi/gameyfin/config/CustomConfiguratioLoader.java @@ -9,13 +9,20 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; import java.util.Objects; +import java.util.Properties; @Configuration -public class SecureProperties { +public class CustomConfiguratioLoader { @Autowired public void setConfigurableEnvironment(ConfigurableEnvironment env) { try { + String firstLibraryPath = env.resolvePlaceholders("gameyfin.root").split(",")[0]; + Properties props = new Properties(); + props.setProperty("gameyfin.db", "%s/.gameyfin/db".formatted(firstLibraryPath)); + props.setProperty("gameyfin.cache", "%s/.gameyfin/cache".formatted(firstLibraryPath)); + env.getPropertySources().addFirst(new PropertiesPropertySource("dynamicallyLoadedGameyfinProperties", props)); + Resource resource = new ClassPathResource("/config/secure.yml"); env.getPropertySources().addFirst(new PropertiesPropertySource(Objects.requireNonNull(resource.getFilename()), PropertiesLoaderUtils.loadProperties(resource))); } catch (Exception ex) { From ba4568cb35fd26f47aed9ad55418e0b30b25a96c Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sat, 13 Aug 2022 12:43:51 +0200 Subject: [PATCH 05/16] Set db and cache path from first library root --- .../gameyfin/config/FilesystemConfig.java | 66 +++++++++++++++++++ ...ratioLoader.java => SecureProperties.java} | 9 +-- .../gameyfin/service/LibraryService.java | 2 +- .../src/main/resources/config/database.yml | 10 +-- .../src/main/resources/config/gameyfin.yml | 9 --- docker/docker-compose.example.yml | 4 -- 6 files changed, 73 insertions(+), 27 deletions(-) create mode 100644 backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java rename backend/src/main/java/de/grimsi/gameyfin/config/{CustomConfiguratioLoader.java => SecureProperties.java} (64%) diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java b/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java new file mode 100644 index 0000000..2c7aa59 --- /dev/null +++ b/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java @@ -0,0 +1,66 @@ +package de.grimsi.gameyfin.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.*; +import org.springframework.util.PropertyPlaceholderHelper; + +import javax.sql.DataSource; +import java.util.Arrays; +import java.util.Properties; +import java.util.stream.StreamSupport; + +@Configuration +public class FilesystemConfig { + + @Value("#{'${gameyfin.root}'.split(',')[0]}") + private String firstLibraryPath; + + @Autowired + Environment env; + + @Autowired + public void setConfigurableEnvironment(ConfigurableEnvironment env) { + Properties props = new Properties(); + props.setProperty("gameyfin.db", "%s/.gameyfin/db".formatted(firstLibraryPath)); + props.setProperty("gameyfin.cache", "%s/.gameyfin/cache".formatted(firstLibraryPath)); + env.getPropertySources().addFirst(new PropertiesPropertySource("gameyfinFilesystemProperties", props)); + } + + /** + * This bean is needed so Spring initializes the data source after we are done messing with the configuration environment + * @return DataSource + */ + @ConfigurationProperties(prefix = "spring.datasource") + @Bean + @Primary + public DataSource getDataSource() { + + Properties properties = loadAllProperties(); + + return DataSourceBuilder + .create() + .url(properties.getProperty("spring.datasource.url")) + .build(); + } + + private Properties loadAllProperties() { + Properties props = new Properties(); + + MutablePropertySources propSrcs = ((AbstractEnvironment) env).getPropertySources(); + + StreamSupport.stream(propSrcs.spliterator(), false) + .filter(ps -> ps instanceof EnumerablePropertySource) + .map(ps -> ((EnumerablePropertySource) ps).getPropertyNames()) + .flatMap(Arrays::stream) + .forEach(propName -> props.setProperty(propName, env.getProperty(propName))); + + return props; + } + +} diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/CustomConfiguratioLoader.java b/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java similarity index 64% rename from backend/src/main/java/de/grimsi/gameyfin/config/CustomConfiguratioLoader.java rename to backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java index 467435f..3140b37 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/config/CustomConfiguratioLoader.java +++ b/backend/src/main/java/de/grimsi/gameyfin/config/SecureProperties.java @@ -9,20 +9,13 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; import java.util.Objects; -import java.util.Properties; @Configuration -public class CustomConfiguratioLoader { +public class SecureProperties { @Autowired public void setConfigurableEnvironment(ConfigurableEnvironment env) { try { - String firstLibraryPath = env.resolvePlaceholders("gameyfin.root").split(",")[0]; - Properties props = new Properties(); - props.setProperty("gameyfin.db", "%s/.gameyfin/db".formatted(firstLibraryPath)); - props.setProperty("gameyfin.cache", "%s/.gameyfin/cache".formatted(firstLibraryPath)); - env.getPropertySources().addFirst(new PropertiesPropertySource("dynamicallyLoadedGameyfinProperties", props)); - Resource resource = new ClassPathResource("/config/secure.yml"); env.getPropertySources().addFirst(new PropertiesPropertySource(Objects.requireNonNull(resource.getFilename()), PropertiesLoaderUtils.loadProperties(resource))); } catch (Exception ex) { diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java index 0f1770c..c0e1efd 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java +++ b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java @@ -39,7 +39,7 @@ public class LibraryService { public List getGameFiles() { List gamefiles = new ArrayList<>(); - libraryFolders.parallelStream().map(Path::of).forEach( + libraryFolders.stream().map(Path::of).forEach( folder -> { try (Stream stream = Files.list(folder)) { // return all sub-folders (non-recursive) and files that have an extension that indicates that they are a downloadable file diff --git a/backend/src/main/resources/config/database.yml b/backend/src/main/resources/config/database.yml index 7c473a2..0e02ae7 100644 --- a/backend/src/main/resources/config/database.yml +++ b/backend/src/main/resources/config/database.yml @@ -1,9 +1,9 @@ spring: jpa: - open-in-view: 'true' + open-in-view: true properties: hibernate: - enable_lazy_load_no_trans: 'true' + enable_lazy_load_no_trans: true event: merge: entity_copy_observer: allow @@ -12,7 +12,7 @@ spring: ddl-auto: update datasource: username: gfadmin - url: jdbc:h2:file:${gameyfin.db}/${spring.datasource.db-name} - driverClassName: org.h2.Driver + password: gameyfin db-name: gameyfin_db - password: gameyfin \ No newline at end of file + url: jdbc:h2:file:${gameyfin.db}/${spring.datasource.db-name} + driverClassName: org.h2.Driver \ No newline at end of file diff --git a/backend/src/main/resources/config/gameyfin.yml b/backend/src/main/resources/config/gameyfin.yml index 68089f9..f70079e 100644 --- a/backend/src/main/resources/config/gameyfin.yml +++ b/backend/src/main/resources/config/gameyfin.yml @@ -1,14 +1,5 @@ gameyfin: - user: - password: - - root: - - db: ./.gameyfin/db - cache: ./.gameyfin/cache - file-extensions: iso, zip, rar, 7z, exe - igdb: api: client-id: diff --git a/docker/docker-compose.example.yml b/docker/docker-compose.example.yml index 5c2e439..265152c 100644 --- a/docker/docker-compose.example.yml +++ b/docker/docker-compose.example.yml @@ -8,10 +8,6 @@ services: - gameyfin.password= - gameyfin.igdb.api.client-id= - gameyfin.igdb.api.client-secret= - # The following two environment variables only need to be set if you have more than one library folder. - # If you have just one you can safely delete them. - - gameyfin.cache=/.gameyfin/cache - - gameyfin.db=/.gameyfin/db volumes: - :/opt/gameyfin-library ports: From c3de83c6b92d369c295e1a0081b50aa45564c99b Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sat, 13 Aug 2022 16:34:56 +0200 Subject: [PATCH 06/16] Renamed "gameyfin.root" to "gameyfin.sources" --- .../java/de/grimsi/gameyfin/config/FilesystemConfig.java | 2 +- .../java/de/grimsi/gameyfin/service/LibraryService.java | 3 +-- .../META-INF/additional-spring-configuration-metadata.json | 2 +- docker/Dockerfile | 6 +++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java b/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java index 2c7aa59..5b9cf98 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java +++ b/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java @@ -18,7 +18,7 @@ import java.util.stream.StreamSupport; @Configuration public class FilesystemConfig { - @Value("#{'${gameyfin.root}'.split(',')[0]}") + @Value("#{'${gameyfin.sources}'.split(',')[0]}") private String firstLibraryPath; @Autowired diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java index c0e1efd..f5c7308 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java +++ b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java @@ -29,9 +29,8 @@ import static de.grimsi.gameyfin.util.FilenameUtil.hasGameArchiveExtension; @RequiredArgsConstructor public class LibraryService { - @Value("${gameyfin.root}") + @Value("${gameyfin.sources}") private List libraryFolders; - private final IgdbWrapper igdbWrapper; private final DetectedGameRepository detectedGameRepository; private final UnmappableFileRepository unmappableFileRepository; diff --git a/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 41bd021..e346e41 100644 --- a/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,7 +1,7 @@ { "properties": [ { - "name": "gameyfin.root", + "name": "gameyfin.sources", "type": "java.lang.String[]", "description": "List of directories Gameyfin should scan for games." } diff --git a/docker/Dockerfile b/docker/Dockerfile index c6077b7..743516b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,10 +1,10 @@ FROM openjdk:18 -ENV GAMEYFIN_ROOT=/opt/gameyfin-library +ENV GAMEYFIN_SOURCES=/opt/gameyfin-library RUN groupadd gameyfin && useradd gameyfin -g gameyfin && \ - mkdir -p /opt/gameyfin ${GAMEYFIN_ROOT} && \ - chown -R gameyfin:gameyfin /opt/gameyfin ${GAMEYFIN_ROOT} + mkdir -p /opt/gameyfin ${GAMEYFIN_SOURCES} && \ + chown -R gameyfin:gameyfin /opt/gameyfin ${GAMEYFIN_SOURCES} USER gameyfin:gameyfin From acd9e79fce694eb504801063224dbd588abae9d0 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sun, 14 Aug 2022 15:26:09 +0200 Subject: [PATCH 07/16] Added field "addedToLibrary" to DetectedGame Integrated Flyway for DB Migrations Removed unused DTO classes --- backend/pom.xml | 8 +- .../config/SecurityConfiguration.java | 1 + .../java/de/grimsi/gameyfin/dto/GameDto.java | 24 --- .../gameyfin/dto/UsernamePasswordDto.java | 9 -- .../gameyfin/entities/DetectedGame.java | 4 + .../src/main/resources/config/database.yml | 4 +- .../V1_0_0__Initial_Database_Setup.sql | 148 ++++++++++++++++++ ...d_Field_addedToLibrary_to_DetectedGame.sql | 4 + .../src/app/models/dtos/DetectedGameDto.ts | 1 + 9 files changed, 168 insertions(+), 35 deletions(-) delete mode 100644 backend/src/main/java/de/grimsi/gameyfin/dto/GameDto.java delete mode 100644 backend/src/main/java/de/grimsi/gameyfin/dto/UsernamePasswordDto.java create mode 100644 backend/src/main/resources/db/migration/V1_0_0__Initial_Database_Setup.sql create mode 100644 backend/src/main/resources/db/migration/V1_1_0__Add_Field_addedToLibrary_to_DetectedGame.sql diff --git a/backend/pom.xml b/backend/pom.xml index 6906b6d..c795310 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -87,6 +87,11 @@ h2 runtime + + org.flywaydb + flyway-core + + @@ -144,8 +149,9 @@ **/*.properties **/*.yml **/*.yaml - **/*.json + **/*.sql **/*.txt + **/*.json **/*.js **/*.css **/*.html diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java b/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java index 22d2812..889989b 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java +++ b/backend/src/main/java/de/grimsi/gameyfin/config/SecurityConfiguration.java @@ -35,6 +35,7 @@ public class SecurityConfiguration { @Bean protected SecurityFilterChain httpSecurity(HttpSecurity http) throws Exception { http.csrf().disable(); + http.headers().frameOptions().disable(); http.httpBasic(Customizer.withDefaults()); return http.build(); } diff --git a/backend/src/main/java/de/grimsi/gameyfin/dto/GameDto.java b/backend/src/main/java/de/grimsi/gameyfin/dto/GameDto.java deleted file mode 100644 index 8f72c3a..0000000 --- a/backend/src/main/java/de/grimsi/gameyfin/dto/GameDto.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.grimsi.gameyfin.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.File; -import java.time.Instant; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class GameDto { - private String name; - private String publisher; - private String slug; - private Instant releaseDate; - - private List files; - private Long fileSize; -} diff --git a/backend/src/main/java/de/grimsi/gameyfin/dto/UsernamePasswordDto.java b/backend/src/main/java/de/grimsi/gameyfin/dto/UsernamePasswordDto.java deleted file mode 100644 index 2f16123..0000000 --- a/backend/src/main/java/de/grimsi/gameyfin/dto/UsernamePasswordDto.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.grimsi.gameyfin.dto; - -import lombok.Data; - -@Data -public class UsernamePasswordDto { - private String username; - private String password; -} diff --git a/backend/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java b/backend/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java index 5d27d91..7435cba 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java +++ b/backend/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java @@ -3,6 +3,7 @@ package de.grimsi.gameyfin.entities; import lombok.*; import org.hibernate.Hibernate; +import org.hibernate.annotations.CreationTimestamp; import javax.persistence.*; import java.time.Instant; @@ -86,6 +87,9 @@ public class DetectedGame { @Column(columnDefinition = "boolean default false") private boolean confirmedMatch; + @CreationTimestamp + private Instant addedToLibrary; + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/backend/src/main/resources/config/database.yml b/backend/src/main/resources/config/database.yml index 0e02ae7..7551f4d 100644 --- a/backend/src/main/resources/config/database.yml +++ b/backend/src/main/resources/config/database.yml @@ -9,7 +9,9 @@ spring: entity_copy_observer: allow database-platform: org.hibernate.dialect.H2Dialect hibernate: - ddl-auto: update + ddl-auto: none + flyway: + baseline-on-migrate: true datasource: username: gfadmin password: gameyfin diff --git a/backend/src/main/resources/db/migration/V1_0_0__Initial_Database_Setup.sql b/backend/src/main/resources/db/migration/V1_0_0__Initial_Database_Setup.sql new file mode 100644 index 0000000..1c3687f --- /dev/null +++ b/backend/src/main/resources/db/migration/V1_0_0__Initial_Database_Setup.sql @@ -0,0 +1,148 @@ +-- Automatically generated by JPA + +-- Hibernate sequence +create sequence HIBERNATE_SEQUENCE start with 1 increment by 1; + +-- Detected Games +create table DETECTED_GAME +( + slug varchar(255) not null, + category varchar(255), + confirmed_match boolean default false, + cover_id varchar(255) not null, + critics_rating integer, + disk_size bigint not null, + lan_support boolean not null, + max_players integer not null, + offline_coop boolean not null, + online_coop boolean not null, + path varchar(255) not null, + release_date timestamp, + summary CLOB, + title varchar(255) not null, + total_rating integer, + user_rating integer, + primary key (slug) +); + +-- Companies +create table COMPANY +( + slug varchar(255) not null, + logo_id varchar(255), + name varchar(255) not null, + primary key (slug) +); + +-- Genres +create table GENRE +( + slug varchar(255) not null, + name varchar(255), + primary key (slug) +); + +-- Themes +create table THEME +( + slug varchar(255) not null, + name varchar(255), + primary key (slug) +); + +-- Keywords +create table KEYWORD +( + slug varchar(255) not null, + name varchar(255), + primary key (slug) +); + +-- Player Perspectives +create table PLAYER_PERSPECTIVE +( + slug varchar(255) not null, + name varchar(255), + primary key (slug) +); + +-- Unmappable files +create table UNMAPPABLE_FILE +( + id bigint not null, + path varchar(255), + primary key (id) +); + +-- Game <-> Companies +create table DETECTED_GAME_COMPANIES +( + detected_game_slug varchar(255) not null, + companies_slug varchar(255) not null +); +alter table DETECTED_GAME_COMPANIES + add constraint companies_company_slug foreign key (companies_slug) references company; +alter table DETECTED_GAME_COMPANIES + add constraint companies_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Genres +create table DETECTED_GAME_GENRES +( + detected_game_slug varchar(255) not null, + genres_slug varchar(255) not null +); +alter table DETECTED_GAME_GENRES + add constraint genres_genre_slug foreign key (genres_slug) references genre; +alter table DETECTED_GAME_GENRES + add constraint genres_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Themes +create table DETECTED_GAME_THEMES +( + detected_game_slug varchar(255) not null, + themes_slug varchar(255) not null +); +alter table DETECTED_GAME_THEMES + add constraint themes_theme_slug foreign key (themes_slug) references theme; +alter table DETECTED_GAME_THEMES + add constraint themes_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Keywords +create table DETECTED_GAME_KEYWORDS +( + detected_game_slug varchar(255) not null, + keywords_slug varchar(255) not null +); +alter table DETECTED_GAME_KEYWORDS + add constraint keywords_keyword_slug foreign key (keywords_slug) references keyword; +alter table DETECTED_GAME_KEYWORDS + add constraint keywords_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Player Perspectives +create table DETECTED_GAME_PLAYER_PERSPECTIVES +( + detected_game_slug varchar(255) not null, + player_perspectives_slug varchar(255) not null +); +alter table DETECTED_GAME_PLAYER_PERSPECTIVES + add constraint player_perspectives_player_perspective_slug foreign key (player_perspectives_slug) references player_perspective; +alter table DETECTED_GAME_PLAYER_PERSPECTIVES + add constraint player_perspectives_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Videos +create table DETECTED_GAME_VIDEO_IDS +( + detected_game_slug varchar(255) not null, + video_ids varchar(255) +); +alter table DETECTED_GAME_VIDEO_IDS + add constraint video_ids_detected_game_slug foreign key (detected_game_slug) references detected_game; + +-- Game <-> Screenshots +create table DETECTED_GAME_SCREENSHOT_IDS +( + detected_game_slug varchar(255) not null, + screenshot_ids varchar(255) +); +alter table DETECTED_GAME_SCREENSHOT_IDS + add constraint screenshot_ids_detected_game_slug foreign key (detected_game_slug) references detected_game; diff --git a/backend/src/main/resources/db/migration/V1_1_0__Add_Field_addedToLibrary_to_DetectedGame.sql b/backend/src/main/resources/db/migration/V1_1_0__Add_Field_addedToLibrary_to_DetectedGame.sql new file mode 100644 index 0000000..e75fe44 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1_1_0__Add_Field_addedToLibrary_to_DetectedGame.sql @@ -0,0 +1,4 @@ +-- Add field "addedToLibrary" to the "DetectedGame" table with the default value of CURRENT_TIMESTAMP() + +alter table DETECTED_GAME +add added_to_library timestamp not null default CURRENT_TIMESTAMP() \ No newline at end of file diff --git a/frontend/src/app/models/dtos/DetectedGameDto.ts b/frontend/src/app/models/dtos/DetectedGameDto.ts index 543c091..ccb7bff 100644 --- a/frontend/src/app/models/dtos/DetectedGameDto.ts +++ b/frontend/src/app/models/dtos/DetectedGameDto.ts @@ -30,4 +30,5 @@ export class DetectedGameDto { path!: string; diskSize!: number; confirmedMatch!: boolean | undefined; + addedToLibrary!: Date; } From 1f24aa73e50e72c6fca8ceb958003f70cef061ae Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sun, 14 Aug 2022 15:46:45 +0200 Subject: [PATCH 08/16] Moved filters into expansion panels --- frontend/src/app/app.module.ts | 76 ++++++++++--------- .../library-overview.component.html | 44 ++++++----- .../library-overview.component.scss | 4 - 3 files changed, 66 insertions(+), 58 deletions(-) diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index ac2dae5..35513e0 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -49,6 +49,7 @@ import {MatListModule} from "@angular/material/list"; import {MatAutocompleteModule} from "@angular/material/autocomplete"; import { NgModelChangeDebouncedDirective } from './directives/ng-model-change-debounced.directive'; import { FooterComponent } from './components/footer/footer.component'; +import {MatExpansionModule} from "@angular/material/expansion"; @NgModule({ declarations: [ @@ -69,43 +70,44 @@ import { FooterComponent } from './components/footer/footer.component'; NgModelChangeDebouncedDirective, FooterComponent ], - imports: [ - BrowserModule, - AppRoutingModule, - BrowserAnimationsModule, - FormsModule, - MatFormFieldModule, - MatCardModule, - MatTabsModule, - MatToolbarModule, - MatMenuModule, - MatIconModule, - HttpClientModule, - FormsModule, - ReactiveFormsModule, - MatDialogModule, - MatButtonModule, - MatInputModule, - FlexModule, - MatProgressSpinnerModule, - MatTableModule, - MatPaginatorModule, - MatSortModule, - MatSnackBarModule, - MatGridListModule, - FlexLayoutModule, - GridModule, - YouTubePlayerModule, - MatChipsModule, - MatTooltipModule, - MatSlideToggleModule, - MatCheckboxModule, - A11yModule, - MatTableFilterModule, - MatDividerModule, - MatListModule, - MatAutocompleteModule - ], + imports: [ + BrowserModule, + AppRoutingModule, + BrowserAnimationsModule, + FormsModule, + MatFormFieldModule, + MatCardModule, + MatTabsModule, + MatToolbarModule, + MatMenuModule, + MatIconModule, + HttpClientModule, + FormsModule, + ReactiveFormsModule, + MatDialogModule, + MatButtonModule, + MatInputModule, + FlexModule, + MatProgressSpinnerModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatSnackBarModule, + MatGridListModule, + FlexLayoutModule, + GridModule, + YouTubePlayerModule, + MatChipsModule, + MatTooltipModule, + MatSlideToggleModule, + MatCheckboxModule, + A11yModule, + MatTableFilterModule, + MatDividerModule, + MatListModule, + MatAutocompleteModule, + MatExpansionModule + ], providers: [ { provide: HTTP_INTERCEPTORS, diff --git a/frontend/src/app/components/library-overview/library-overview.component.html b/frontend/src/app/components/library-overview/library-overview.component.html index 173a6ef..adf3dc4 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.html +++ b/frontend/src/app/components/library-overview/library-overview.component.html @@ -27,7 +27,7 @@
search - + @@ -37,12 +37,15 @@
-
-
-

Gamemodes

- error -
-
+ + + +

Gamemodes

+ error +
+
+ +
Offline Co-op Online Co-op @@ -50,28 +53,35 @@ LAN Support
-
+ -
-

Genres

-
+ + +

Genres

+
+ +
{{genre.name}}
-
+ -
-

Themes

-
+ + +

Themes

+
+ +
{{theme.name}}
-
- +
+
+
diff --git a/frontend/src/app/components/library-overview/library-overview.component.scss b/frontend/src/app/components/library-overview/library-overview.component.scss index 92d78a1..d5703ae 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.scss +++ b/frontend/src/app/components/library-overview/library-overview.component.scss @@ -44,10 +44,6 @@ margin-bottom: 0; } -.filter-category-content { - margin-left: 6px; -} - ::ng-deep .mat-checkbox-frame { $config: mat.get-color-config($custom-theme); $primary-palette: map.get($config, 'primary'); From 9ff6d76cf293e842117e444996ed760a4f58fbea Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 15 Aug 2022 14:31:44 +0200 Subject: [PATCH 09/16] Refactored and improved dark-mode implementation --- frontend/src/app/app.module.ts | 2 +- .../components/footer/footer.component.scss | 4 +- .../components/header/header.component.html | 3 +- .../app/components/header/header.component.ts | 69 +++--------------- .../library-management.component.scss | 9 --- .../library-management.component.ts | 4 +- .../library-overview.component.html | 29 +++++--- .../library-overview.component.scss | 16 ++-- .../library-overview.component.ts | 11 ++- .../src/app/services/cookie.service.spec.ts | 16 ++++ frontend/src/app/services/cookie.service.ts | 34 +++++++++ .../src/app/services/theming.service.spec.ts | 16 ++++ frontend/src/app/services/theming.service.ts | 53 ++++++++++++++ .../dark-theme.scss} | 2 +- frontend/src/app/themes/light-theme.scss | 14 ++++ .../src/assets/Gameyfin_Logo_256px_dark.png | Bin 0 -> 3006 bytes frontend/src/styles.scss | 16 ++-- 17 files changed, 192 insertions(+), 106 deletions(-) create mode 100644 frontend/src/app/services/cookie.service.spec.ts create mode 100644 frontend/src/app/services/cookie.service.ts create mode 100644 frontend/src/app/services/theming.service.spec.ts create mode 100644 frontend/src/app/services/theming.service.ts rename frontend/src/app/{theme/default-theme.scss => themes/dark-theme.scss} (90%) create mode 100644 frontend/src/app/themes/light-theme.scss create mode 100644 frontend/src/assets/Gameyfin_Logo_256px_dark.png diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 35513e0..cf3c62d 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -121,7 +121,7 @@ import {MatExpansionModule} from "@angular/material/expansion"; }, { provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, - useValue: { panelClass: ['snackbar-dark'] }, + useValue: { panelClass: ['formatted-snackbar'] }, } ], bootstrap: [AppComponent] diff --git a/frontend/src/app/components/footer/footer.component.scss b/frontend/src/app/components/footer/footer.component.scss index b7d02fb..47698af 100644 --- a/frontend/src/app/components/footer/footer.component.scss +++ b/frontend/src/app/components/footer/footer.component.scss @@ -1,9 +1,9 @@ @use 'sass:map'; @use '@angular/material' as mat; -@import '../../theme/default-theme'; +@import 'src/app/themes/light-theme'; a { - $config: mat.get-color-config($custom-theme); + $config: mat.get-color-config($light-theme); $primary-palette: map.get($config, 'primary'); color: mat.get-color-from-palette($primary-palette, 500); } diff --git a/frontend/src/app/components/header/header.component.html b/frontend/src/app/components/header/header.component.html index dd877db..ab0068b 100644 --- a/frontend/src/app/components/header/header.component.html +++ b/frontend/src/app/components/header/header.component.html @@ -5,7 +5,8 @@ - + + + + + + + {{game.title}} -
+ @@ -46,11 +55,11 @@
- Offline Co-op + Offline Co-op - Online Co-op + Online Co-op - LAN Support + LAN Support
@@ -80,7 +89,7 @@
-
+
diff --git a/frontend/src/app/components/library-overview/library-overview.component.scss b/frontend/src/app/components/library-overview/library-overview.component.scss index d5703ae..28825b2 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.scss +++ b/frontend/src/app/components/library-overview/library-overview.component.scss @@ -1,6 +1,6 @@ @use 'sass:map'; @use '@angular/material' as mat; -@import '../../theme/default-theme'; +@import 'src/app/themes/dark-theme'; .fullscreen-overlay { position: absolute; @@ -19,21 +19,15 @@ @include mat.elevation(16); - $config: mat.get-color-config($custom-theme); - $background: map.get($config, background); position: absolute; right: 56px; top: 72px; width: 250px; border-radius: 6px; - background: mat.get-color-from-palette($background, app-bar); - border-color: mat.get-color-from-palette($background, app-bar); - border-style: solid; - color: white; p { padding: 0 12px 12px 16px; - } + } } .content { @@ -45,13 +39,13 @@ } ::ng-deep .mat-checkbox-frame { - $config: mat.get-color-config($custom-theme); + $config: mat.get-color-config($dark-theme); $primary-palette: map.get($config, 'primary'); - border-color: mat.get-color-from-palette($primary-palette, 500); + border-color: mat.get-color-from-palette($primary-palette, 500) !important; } ::ng-deep .mat-form-field-underline { - $config: mat.get-color-config($custom-theme); + $config: mat.get-color-config($dark-theme); $primary-palette: map.get($config, 'primary'); background-color: mat.get-color-from-palette($primary-palette, 500) !important; } diff --git a/frontend/src/app/components/library-overview/library-overview.component.ts b/frontend/src/app/components/library-overview/library-overview.component.ts index 774ad40..e5b176f 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.ts +++ b/frontend/src/app/components/library-overview/library-overview.component.ts @@ -46,13 +46,22 @@ export class LibraryOverviewComponent implements AfterContentInit { forkJoin([themeObservable, genreObservable]).subscribe(result => { this.availableThemes = result[0]; this.availableGenres = result[1]; - this.filterGames(); + this.refreshLibraryView(); this.loading = false; }); } ); } + refreshLibraryView(): void { + this.filterGames(); + } + + clearSearchTerm(): void { + this.searchTerm = ""; + this.refreshLibraryView(); + } + filterGames(): void { this.gameServerService.getAllGames().subscribe(games => { let filteredGames: DetectedGameDto[] = games; diff --git a/frontend/src/app/services/cookie.service.spec.ts b/frontend/src/app/services/cookie.service.spec.ts new file mode 100644 index 0000000..43ea274 --- /dev/null +++ b/frontend/src/app/services/cookie.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CookieService } from './cookie.service'; + +describe('CookieService', () => { + let service: CookieService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CookieService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/cookie.service.ts b/frontend/src/app/services/cookie.service.ts new file mode 100644 index 0000000..00af0e3 --- /dev/null +++ b/frontend/src/app/services/cookie.service.ts @@ -0,0 +1,34 @@ +import {Injectable} from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CookieService { + + constructor() { + } + + setCookie(name: string, value: any): void { + document.cookie = `${name}=${value.toString()};`; + } + + getCookie(name: string): string | null { + let end; + const dc = document.cookie; + const prefix = name + "="; + let begin = dc.indexOf("; " + prefix); + + if (begin == -1) { + begin = dc.indexOf(prefix); + if (begin != 0) return null; + } else { + begin += 2; + end = document.cookie.indexOf(";", begin); + if (end == -1) { + end = dc.length; + } + } + + return decodeURI(dc.substring(begin + prefix.length, end)); + } +} diff --git a/frontend/src/app/services/theming.service.spec.ts b/frontend/src/app/services/theming.service.spec.ts new file mode 100644 index 0000000..f932a5e --- /dev/null +++ b/frontend/src/app/services/theming.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ThemingService } from './theming.service'; + +describe('ThemingService', () => { + let service: ThemingService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ThemingService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/theming.service.ts b/frontend/src/app/services/theming.service.ts new file mode 100644 index 0000000..787718b --- /dev/null +++ b/frontend/src/app/services/theming.service.ts @@ -0,0 +1,53 @@ +import {Injectable} from '@angular/core'; +import {OverlayContainer} from "@angular/cdk/overlay"; +import {CookieService} from "./cookie.service"; + +@Injectable({ + providedIn: 'root' +}) +export class ThemingService { + + private darkmodeEnabled!: boolean; + private darkmodeClassName: string = 'darkMode'; + + constructor(private cookieService: CookieService, + private overlay: OverlayContainer) { + if (this.cookieService.getCookie("darkmode") !== null) { + this.darkmodeEnabled = this.cookieService.getCookie("darkmode") === "true"; + } else if (window.matchMedia) { + this.darkmodeEnabled = window.matchMedia('(prefers-color-scheme: dark)').matches; + } else { + this.darkmodeEnabled = false; + } + + this.setTheme(); + } + + toggleTheme(): void { + this.darkmodeEnabled = !this.darkmodeEnabled; + this.setTheme(); + } + + private setTheme(): void { + this.darkmodeEnabled ? this.setDarkmode() : this.setLightmode(); + this.cookieService.setCookie("darkmode", this.darkmodeEnabled); + } + + private setDarkmode(): void { + document.body.classList.add(this.darkmodeClassName); + document.body.style.colorScheme = "dark"; + document.body.style.background = "#303030"; + document.body.style.color = "white"; + + this.overlay.getContainerElement().classList.add(this.darkmodeClassName); + } + + private setLightmode(): void { + document.body.classList.remove(this.darkmodeClassName); + document.body.style.colorScheme = "light"; + document.body.style.background = "white"; + document.body.style.color = "black"; + + this.overlay.getContainerElement().classList.remove(this.darkmodeClassName); + } +} diff --git a/frontend/src/app/theme/default-theme.scss b/frontend/src/app/themes/dark-theme.scss similarity index 90% rename from frontend/src/app/theme/default-theme.scss rename to frontend/src/app/themes/dark-theme.scss index 93993e4..e0a9323 100644 --- a/frontend/src/app/theme/default-theme.scss +++ b/frontend/src/app/themes/dark-theme.scss @@ -5,7 +5,7 @@ $custom-theme-primary: mat.define-palette(mat.$green-palette); $custom-theme-accent: mat.define-palette(mat.$grey-palette, A200, A100, A400); $custom-theme-warn: mat.define-palette(mat.$red-palette); -$custom-theme: mat.define-dark-theme(( +$dark-theme: mat.define-dark-theme(( color: ( primary: $custom-theme-primary, accent: $custom-theme-accent, diff --git a/frontend/src/app/themes/light-theme.scss b/frontend/src/app/themes/light-theme.scss new file mode 100644 index 0000000..83897e1 --- /dev/null +++ b/frontend/src/app/themes/light-theme.scss @@ -0,0 +1,14 @@ +@use '@angular/material' as mat; +@import "@angular/material/theming"; + +$custom-theme-primary: mat.define-palette(mat.$green-palette); +$custom-theme-accent: mat.define-palette(mat.$grey-palette, A200, A100, A400); +$custom-theme-warn: mat.define-palette(mat.$red-palette); + +$light-theme: mat.define-light-theme(( + color: ( + primary: $custom-theme-primary, + accent: $custom-theme-accent, + warn: $custom-theme-warn + ) +)); diff --git a/frontend/src/assets/Gameyfin_Logo_256px_dark.png b/frontend/src/assets/Gameyfin_Logo_256px_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b6be344adb9bb372cf785587cb104d13ac9c76bc GIT binary patch literal 3006 zcmcgu`#+QIAHR0M@B8{(*XM9upU?aLysv9M``uMmtz88GQ1RHi z+ZO-?JOtk{^6>l4~`_LkpF|{9HZNgZ4A zId!>oa*jOLpbx1V2t0Rr_f$O}jC~+!Uzi?50sU}>f+6jddb2T2_{+>WvFM0p>$jvD z{qZX8W$k?cq_?|^y;m+ineNd5P@HA!?U&uZ1ig_Fp&AH)TNCkp`{GM?F$DuSP;z!- zkTBNstR(`NW?xCX^3qSDZBu&#fb3_XT#qwY1SlV^q3=f`04C3u>tO&O`YnIbY78i< z_Qe*Uk>C(3kYI8N0OK2f^Eavk4kr5dau}5Q+v@lib9+TJ=zo7yW;{eMkI&lP04 zgCZTvRzKtAZrI+HAu^>2Z`~cYRz@6KpD>t=242O2kW>H#B@L$xOq$GXv_Nsf#XoA9 z7Zdc;QV;{H813>-ghX9eJB@)XlMj?Paf;A^`DD6apXVu~I_(GsZ#bom81u(FU=cN1 z%CB_8xZfaK^isC0xMF~#dc+Ieq_dQ4-(SX06lR0|HDNb)qEaE;j+3GkFSD~H^G7Qs zEh|pj?NA(D?IfYe2IA6F(lX)jxB2^$FRMZN6pue+Q9?Sz@1Ym`@>{^h#XTDG^Nd_} zz7C=oob+ue-ev_#0-MQ(iON}>?o1^ zXlq1Rm#1z0i+3rdn8^nBBfR{N--?&Y_w7J(z@|=S)(b(YVBP$ar$cFnQ#F968Ck2~ z(3m?fu`M9B>ELG*G_;z2mtYboKCr;6;Loc}dm4hkm&039mK`dLa87}%z zs)3bh8Gp|{vt8VWTUGiy9$kd-Vg+feIY=@dTTVKCMwE6C1{9>n@FxWp=zK0M3nV@H zsnCtapR8tDnK?J?n;JeCz`gk}*5ZdfP=>Ak!DV7UQciEKuX~{U;D{XF2wUJ$Cyus56!6O0dU~X=fG_EpeA-9}AZ)TGdvkjC7k}4*9C^zz0d_kFI&Q5*cPr9i1 zk>VXlT7CIc76MO*-1D}+N=)BBJXm`T6fqx_y7InNGP8av<{q#h*mSssVHR zo2Lgq%~E;pgvgd*ID+4_+gn1;RD)oU6KQTwKFQO#N`J>l%YN@tk^Z)HWjmYyeJvJ_ zVVuxApirTk9xZapS^olE#EfnxYf0UdA57QpLJ13L>Eau{tI0R2&MQAiBU`zx(&zhRrkmD8(m~@L>R%H_EeE2*ERHB3y5wsj z#U9}nNAwY0sUuqE&gS+>XowPefBnc26qZ+y0ezJf0aSldy*jA9=GaO$Gq-<<0neRI zS1iHlF#u;K%^MejYxC*vwjntg4v8Ow$YpH4*YlTsI}k;8@*JjoxVg2nF>lb&w!ha? zT?mn1SkJd=f+@cRVH$4a7X4j*P-2KN7!A6xD3?WEruU!C1=qR#ueMjyhph~eMH$BW zR76})q$?V)5ylK;% z*T259HS|f>U4cl0KeOAA9klZn8ej|6tHmp7;{b*hXrY;R2|xiIv{eQmtVg4F_RaF~ zWjTczBLhdaQjMtxoUa7ba^^2<(|bAz}nKAbT^OsAB!}=GHr~A?fb4oOG4u}B@H)@9U*ry0Lx(e zid8)&${l=&Ywk-thCuk>67;tif>lhxWg9f!Flm^`Q}Dw;$QcvyyN*WnXKo-{bfaEP z3udVZG#J7mfZZMdGX4)@wGX3)Y`xg}GYtwMin&D1h~)LIb{(O-=9FWnB$u3!VKm%` zOKekPU}FW*aq=I@HRDth`o_tO3c9dK&oY{V8OXPc6HZIZ#PcU>+QYCm(PO36RPTc zj=+grzP{Bi7#UVQGv&RMbhm*hV-!EnZv%;YRMsMgF0y7KM9)b#Q^xz?gZH5CsjDEW zKZ`Cu5X^p!j)ftmU3(rR>;ApOlH{H52@!@&J4Bp~md76yzigD=`9OJF*Pzos7M+xu zNH52N=amw+8h2Ef7sbAI*H^G-CG4L=~jKct;8iZf9OlT6(p#a?ilRs93C!Zq8_pl z!n(3LMh_g2qw2Q}Q8~kIoR^3Xa1ln{f1o~%g05$aFd}tYAOcu)Ydkx@cJWdIo=!Tu(Ph`sB7gkoBl|I4}R761t|4>^#EfYBO^K$s77E-M1 zBKfO#F%z}sGZv&M8ZH%go%H1*JXPd4vEt}$=JuSc5`=JCy;(m}mIugX8TQ}bsARZw ztmRD+6ScNW4HEFBvxT?ezGOXa3|6eFWqWK*KnMrbo2S`)kWx4 Date: Mon, 15 Aug 2022 15:49:19 +0200 Subject: [PATCH 10/16] Implemented sorting for library overview --- frontend/src/app/app.module.ts | 78 +++++++-------- .../library-overview.component.html | 23 ++++- .../library-overview.component.scss | 4 + .../library-overview.component.ts | 97 +++++++++++++------ .../navbar-layout/navbar-layout.component.ts | 2 +- 5 files changed, 132 insertions(+), 72 deletions(-) diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index cf3c62d..bc2d9d4 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -50,6 +50,7 @@ import {MatAutocompleteModule} from "@angular/material/autocomplete"; import { NgModelChangeDebouncedDirective } from './directives/ng-model-change-debounced.directive'; import { FooterComponent } from './components/footer/footer.component'; import {MatExpansionModule} from "@angular/material/expansion"; +import {MatSelectModule} from "@angular/material/select"; @NgModule({ declarations: [ @@ -70,44 +71,45 @@ import {MatExpansionModule} from "@angular/material/expansion"; NgModelChangeDebouncedDirective, FooterComponent ], - imports: [ - BrowserModule, - AppRoutingModule, - BrowserAnimationsModule, - FormsModule, - MatFormFieldModule, - MatCardModule, - MatTabsModule, - MatToolbarModule, - MatMenuModule, - MatIconModule, - HttpClientModule, - FormsModule, - ReactiveFormsModule, - MatDialogModule, - MatButtonModule, - MatInputModule, - FlexModule, - MatProgressSpinnerModule, - MatTableModule, - MatPaginatorModule, - MatSortModule, - MatSnackBarModule, - MatGridListModule, - FlexLayoutModule, - GridModule, - YouTubePlayerModule, - MatChipsModule, - MatTooltipModule, - MatSlideToggleModule, - MatCheckboxModule, - A11yModule, - MatTableFilterModule, - MatDividerModule, - MatListModule, - MatAutocompleteModule, - MatExpansionModule - ], + imports: [ + BrowserModule, + AppRoutingModule, + BrowserAnimationsModule, + FormsModule, + MatFormFieldModule, + MatCardModule, + MatTabsModule, + MatToolbarModule, + MatMenuModule, + MatIconModule, + HttpClientModule, + FormsModule, + ReactiveFormsModule, + MatDialogModule, + MatButtonModule, + MatInputModule, + FlexModule, + MatProgressSpinnerModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatSnackBarModule, + MatGridListModule, + FlexLayoutModule, + GridModule, + YouTubePlayerModule, + MatChipsModule, + MatTooltipModule, + MatSlideToggleModule, + MatCheckboxModule, + A11yModule, + MatTableFilterModule, + MatDividerModule, + MatListModule, + MatAutocompleteModule, + MatExpansionModule, + MatSelectModule + ], providers: [ { provide: HTTP_INTERCEPTORS, diff --git a/frontend/src/app/components/library-overview/library-overview.component.html b/frontend/src/app/components/library-overview/library-overview.component.html index 8f84cca..88a3c49 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.html +++ b/frontend/src/app/components/library-overview/library-overview.component.html @@ -26,8 +26,9 @@
- - @@ -46,6 +47,15 @@ + +

Sort by:

+ + + {{sortOption.title}} + + +
+ @@ -55,11 +65,14 @@
- Offline Co-op + Offline + Co-op - Online Co-op + Online + Co-op - LAN Support + LAN + Support
diff --git a/frontend/src/app/components/library-overview/library-overview.component.scss b/frontend/src/app/components/library-overview/library-overview.component.scss index 28825b2..c3d9f7e 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.scss +++ b/frontend/src/app/components/library-overview/library-overview.component.scss @@ -38,6 +38,10 @@ margin-bottom: 0; } +.mat-card-48 { + height: 48px; +} + ::ng-deep .mat-checkbox-frame { $config: mat.get-color-config($dark-theme); $primary-palette: map.get($config, 'primary'); diff --git a/frontend/src/app/components/library-overview/library-overview.component.ts b/frontend/src/app/components/library-overview/library-overview.component.ts index e5b176f..8e9c1b9 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.ts +++ b/frontend/src/app/components/library-overview/library-overview.component.ts @@ -1,9 +1,22 @@ -import {AfterContentInit, AfterViewInit, Component, Input} from '@angular/core'; +import {AfterContentInit, Component} from '@angular/core'; import {GamesService} from "../../services/games.service"; import {DetectedGameDto} from "../../models/dtos/DetectedGameDto"; import {GenreDto} from "../../models/dtos/GenreDto"; import {ThemeDto} from "../../models/dtos/ThemeDto"; -import {forkJoin, Observable} from "rxjs"; +import {firstValueFrom, forkJoin, Observable, pipe} from "rxjs"; +import {SortDirection} from "@angular/material/sort"; + +class SortOption { + title: string; + field: string; + direction: SortDirection; + + constructor(title: string, field: string, direction: SortDirection) { + this.title = title; + this.field = field; + this.direction = direction; + } +} @Component({ selector: 'app-gameserver-list', @@ -12,7 +25,24 @@ import {forkJoin, Observable} from "rxjs"; }) export class LibraryOverviewComponent implements AfterContentInit { + defaultSortOption: SortOption = new SortOption("Title (A-Z)", "title", "asc"); + + sortOptions: SortOption[] = [ + this.defaultSortOption, + new SortOption("Title (Z-A)", "title", "desc"), + + new SortOption("Release (newest first)", "releaseDate", "desc"), + new SortOption("Release (oldest first)", "releaseDate", "asc"), + + new SortOption("Added to library (newest first)", "addedToLibrary", "desc"), + new SortOption("Added to library (oldest first)", "addedToLibrary", "asc"), + + new SortOption("Rating (highest first)", "totalRating", "desc"), + new SortOption("Rating (lowest first)", "totalRating", "asc") + ]; + searchTerm: string = ""; + selectedSortOption: SortOption = this.defaultSortOption; offlineCoopFilterEnabled: boolean = false; onlineCoopFilterEnabled: boolean = false; lanSupportFilterEnabled: boolean = false; @@ -32,7 +62,7 @@ export class LibraryOverviewComponent implements AfterContentInit { ngAfterContentInit(): void { this.gameServerService.getAllGames().subscribe( detectedGames => { - if(detectedGames.length === 0) { + if (detectedGames.length === 0) { this.gameLibraryIsEmpty = true; this.loading = false; return; @@ -46,15 +76,15 @@ export class LibraryOverviewComponent implements AfterContentInit { forkJoin([themeObservable, genreObservable]).subscribe(result => { this.availableThemes = result[0]; this.availableGenres = result[1]; - this.refreshLibraryView(); - this.loading = false; + this.refreshLibraryView().then(() => this.loading = false); }); } ); } - refreshLibraryView(): void { - this.filterGames(); + async refreshLibraryView(): Promise { + let games: DetectedGameDto[] = await firstValueFrom(this.gameServerService.getAllGames()); + this.games = this.sortGames(this.filterGames(games)); } clearSearchTerm(): void { @@ -62,32 +92,43 @@ export class LibraryOverviewComponent implements AfterContentInit { this.refreshLibraryView(); } - filterGames(): void { - this.gameServerService.getAllGames().subscribe(games => { - let filteredGames: DetectedGameDto[] = games; + filterGames(games: DetectedGameDto[]): DetectedGameDto[] { + if (this.searchTerm.trim().toLowerCase().length > 0) { + games = games.filter(game => game.title.trim().toLowerCase().includes(this.searchTerm.trim().toLowerCase())); + } - if(this.searchTerm.trim().toLowerCase().length > 0) { - filteredGames = filteredGames.filter(game => game.title.trim().toLowerCase().includes(this.searchTerm.trim().toLowerCase())); - } + if (this.offlineCoopFilterEnabled || this.onlineCoopFilterEnabled || this.lanSupportFilterEnabled) { + games = games.filter(game => (game.offlineCoop === this.offlineCoopFilterEnabled || game.onlineCoop === this.onlineCoopFilterEnabled || game.lanSupport === this.lanSupportFilterEnabled)); + } - if(this.offlineCoopFilterEnabled || this.onlineCoopFilterEnabled || this.lanSupportFilterEnabled) { - filteredGames = filteredGames.filter(game => (game.offlineCoop === this.offlineCoopFilterEnabled || game.onlineCoop === this.onlineCoopFilterEnabled || game.lanSupport === this.lanSupportFilterEnabled)); - } + if (this.activeGenreFilters.length > 0) { + games = games.filter(game => this.activeGenreFilters.every(activeGenreFilter => game.genres?.map(g => g.slug).includes(activeGenreFilter))); + } - if(this.activeGenreFilters.length > 0) { - filteredGames = filteredGames.filter(game => this.activeGenreFilters.every(activeGenreFilter => game.genres?.map(g => g.slug).includes(activeGenreFilter))); - } + if (this.activeThemeFilters.length > 0) { + games = games.filter(game => this.activeThemeFilters.every(activeThemeFilter => game.themes?.map(g => g.slug).includes(activeThemeFilter))); + } - if(this.activeThemeFilters.length > 0) { - filteredGames = filteredGames.filter(game => this.activeThemeFilters.every(activeThemeFilter => game.themes?.map(g => g.slug).includes(activeThemeFilter))); - } + return games; + } - this.games = filteredGames; - }) + sortGames(games: DetectedGameDto[]): DetectedGameDto[] { + games = games.sort((g1, g2) => { + // @ts-ignore + let f1 = g1[this.selectedSortOption.field]; + // @ts-ignore + let f2 = g2[this.selectedSortOption.field]; + + if(f1 > f2) return 1; + if(f1 < f2) return -1; + return 0; + }); + if(this.selectedSortOption.direction === "desc") games = games.reverse(); + return games; } toggleGenreFilter(slug: string): void { - if(this.activeGenreFilters.includes(slug)) { + if (this.activeGenreFilters.includes(slug)) { const index = this.activeGenreFilters.indexOf(slug, 0); if (index > -1) { @@ -98,11 +139,11 @@ export class LibraryOverviewComponent implements AfterContentInit { this.activeGenreFilters.push(slug); } - this.filterGames(); + this.refreshLibraryView(); } toggleThemeFilter(slug: string) { - if(this.activeThemeFilters.includes(slug)) { + if (this.activeThemeFilters.includes(slug)) { const index = this.activeThemeFilters.indexOf(slug, 0); if (index > -1) { @@ -113,7 +154,7 @@ export class LibraryOverviewComponent implements AfterContentInit { this.activeThemeFilters.push(slug); } - this.filterGames(); + this.refreshLibraryView(); } } diff --git a/frontend/src/app/layouts/navbar-layout/navbar-layout.component.ts b/frontend/src/app/layouts/navbar-layout/navbar-layout.component.ts index 9ce31f1..397b20a 100644 --- a/frontend/src/app/layouts/navbar-layout/navbar-layout.component.ts +++ b/frontend/src/app/layouts/navbar-layout/navbar-layout.component.ts @@ -4,7 +4,7 @@ import {Component, OnInit} from '@angular/core'; selector: 'app-navbar-layout', template: `
-
+
From b6626c9d4faac0a68ca80ffb508b54d2a30cdcec Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:10:12 +0200 Subject: [PATCH 11/16] Added filter for player perspective --- .../library-overview.component.html | 12 +++++++ .../library-overview.component.ts | 32 +++++++++++++++++-- .../app/models/dtos/PlayerPerspectiveDto.ts | 2 +- frontend/src/app/services/games.service.ts | 18 ++++++++++- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/library-overview/library-overview.component.html b/frontend/src/app/components/library-overview/library-overview.component.html index 88a3c49..d741353 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.html +++ b/frontend/src/app/components/library-overview/library-overview.component.html @@ -100,6 +100,18 @@ color="primary">{{theme.name}}
+ + + +

Player Perspectives

+
+ +
+ {{playerPerspective.name}} +
+
diff --git a/frontend/src/app/components/library-overview/library-overview.component.ts b/frontend/src/app/components/library-overview/library-overview.component.ts index 8e9c1b9..a8efcd1 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.ts +++ b/frontend/src/app/components/library-overview/library-overview.component.ts @@ -5,6 +5,8 @@ import {GenreDto} from "../../models/dtos/GenreDto"; import {ThemeDto} from "../../models/dtos/ThemeDto"; import {firstValueFrom, forkJoin, Observable, pipe} from "rxjs"; import {SortDirection} from "@angular/material/sort"; +import {CompanyDto} from "../../models/dtos/CompanyDto"; +import {PlayerPerspectiveDto} from "../../models/dtos/PlayerPerspectiveDto"; class SortOption { title: string; @@ -48,10 +50,12 @@ export class LibraryOverviewComponent implements AfterContentInit { lanSupportFilterEnabled: boolean = false; activeThemeFilters: string[] = []; activeGenreFilters: string[] = []; + activePlayerPerspectiveFilters: string[] = []; games: DetectedGameDto[] = []; availableGenres: GenreDto[] = []; availableThemes: ThemeDto[] = []; + availablePlayerPerspectives: PlayerPerspectiveDto[] = []; loading: boolean = true; gameLibraryIsEmpty: boolean = false; @@ -72,10 +76,13 @@ export class LibraryOverviewComponent implements AfterContentInit { let genreObservable: Observable = this.gameServerService.getAvailableGenres(); let themeObservable: Observable = this.gameServerService.getAvailableThemes(); + let playerPerspectiveObservable: Observable = this.gameServerService.getAvailablePlayerPerspectives(); + + forkJoin([genreObservable, themeObservable, playerPerspectiveObservable]).subscribe(result => { + this.availableGenres = result[0]; + this.availableThemes = result[1]; + this.availablePlayerPerspectives = result[2]; - forkJoin([themeObservable, genreObservable]).subscribe(result => { - this.availableThemes = result[0]; - this.availableGenres = result[1]; this.refreshLibraryView().then(() => this.loading = false); }); } @@ -109,6 +116,10 @@ export class LibraryOverviewComponent implements AfterContentInit { games = games.filter(game => this.activeThemeFilters.every(activeThemeFilter => game.themes?.map(g => g.slug).includes(activeThemeFilter))); } + if (this.activePlayerPerspectiveFilters.length > 0) { + games = games.filter(game => this.activePlayerPerspectiveFilters.every(activePlayerPerspectiveFilter => game.playerPerspectives?.map(g => g.slug).includes(activePlayerPerspectiveFilter))); + } + return games; } @@ -157,4 +168,19 @@ export class LibraryOverviewComponent implements AfterContentInit { this.refreshLibraryView(); } + togglePlayerPerspectiveFilter(slug: string) { + if (this.activePlayerPerspectiveFilters.includes(slug)) { + + const index = this.activePlayerPerspectiveFilters.indexOf(slug, 0); + if (index > -1) { + this.activePlayerPerspectiveFilters.splice(index, 1); + } + + } else { + this.activePlayerPerspectiveFilters.push(slug); + } + + this.refreshLibraryView(); + } + } diff --git a/frontend/src/app/models/dtos/PlayerPerspectiveDto.ts b/frontend/src/app/models/dtos/PlayerPerspectiveDto.ts index ff0ee82..e4f7e0d 100644 --- a/frontend/src/app/models/dtos/PlayerPerspectiveDto.ts +++ b/frontend/src/app/models/dtos/PlayerPerspectiveDto.ts @@ -1,4 +1,4 @@ export class PlayerPerspectiveDto { slug!: string; - name?: string; + name!: string; } diff --git a/frontend/src/app/services/games.service.ts b/frontend/src/app/services/games.service.ts index c91f940..87bd6f4 100644 --- a/frontend/src/app/services/games.service.ts +++ b/frontend/src/app/services/games.service.ts @@ -6,6 +6,8 @@ import {DetectedGameDto} from "../models/dtos/DetectedGameDto"; import {GameOverviewDto} from "../models/dtos/GameOverviewDto"; import {GenreDto} from "../models/dtos/GenreDto"; import {ThemeDto} from "../models/dtos/ThemeDto"; +import {CompanyDto} from "../models/dtos/CompanyDto"; +import {PlayerPerspectiveDto} from "../models/dtos/PlayerPerspectiveDto"; @Injectable({ providedIn: 'root' @@ -64,7 +66,7 @@ export class GamesService implements GamesApi { return this.getAllGames().pipe( map( games => { - let availableThemesMap: Map = new Map; + let availableThemesMap: Map = new Map; games.map(game => game.themes === undefined ? [] : game.themes).flat().forEach(theme => availableThemesMap.set(theme.slug, theme)); return Array.from(availableThemesMap.values()).sort((t1, t2) => t1.name.localeCompare(t2.name)); } @@ -72,6 +74,20 @@ export class GamesService implements GamesApi { ); } + // TODO: This method of removing duplicates is most certainly an anti-pattern in RxJS + // TODO: However, I did not get the 'distinct()' pipe to work properly, so I have to take another look in the future + getAvailablePlayerPerspectives(): Observable { + return this.getAllGames().pipe( + map( + games => { + let availablePlayerPerspectivesMap: Map = new Map; + games.map(game => game.playerPerspectives === undefined ? [] : game.playerPerspectives).flat().forEach(playerPerspective => availablePlayerPerspectivesMap.set(playerPerspective.slug, playerPerspective)); + return Array.from(availablePlayerPerspectivesMap.values()).sort((t1, t2) => t1.name.localeCompare(t2.name)); + } + ) + ); + } + downloadGame(slug: String): void { window.open(`v1${this.apiPath}/game/${slug}/download`, '_top'); } From b58af494e9d00da1b50fad285bf1ac35b8c9e435 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:09:59 +0200 Subject: [PATCH 12/16] Implement loading and saving state from/to URL --- .../app/components/header/header.component.ts | 7 +- .../library-overview.component.ts | 76 +++++++++++++++++-- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts index 130ca42..5c8c67f 100644 --- a/frontend/src/app/components/header/header.component.ts +++ b/frontend/src/app/components/header/header.component.ts @@ -5,7 +5,7 @@ import {timeInterval} from "rxjs"; import {Router} from "@angular/router"; import {GamesService} from "../../services/games.service"; import {ThemingService} from "../../services/theming.service"; -import {DOCUMENT} from '@angular/common'; +import {DOCUMENT, Location} from '@angular/common'; @Component({ selector: 'app-header', @@ -21,7 +21,8 @@ export class HeaderComponent { private gameService: GamesService, private themingService: ThemingService, private snackBar: MatSnackBar, - private router: Router) { + private router: Router, + private location: Location) { } scanLibrary(): void { @@ -42,7 +43,7 @@ export class HeaderComponent { } goToLibraryScreen(): void { - this.router.navigate(['/']); + this.location.back(); } goToLibraryManagementScreen(): void { diff --git a/frontend/src/app/components/library-overview/library-overview.component.ts b/frontend/src/app/components/library-overview/library-overview.component.ts index a8efcd1..c40b4f7 100644 --- a/frontend/src/app/components/library-overview/library-overview.component.ts +++ b/frontend/src/app/components/library-overview/library-overview.component.ts @@ -3,10 +3,12 @@ import {GamesService} from "../../services/games.service"; import {DetectedGameDto} from "../../models/dtos/DetectedGameDto"; import {GenreDto} from "../../models/dtos/GenreDto"; import {ThemeDto} from "../../models/dtos/ThemeDto"; -import {firstValueFrom, forkJoin, Observable, pipe} from "rxjs"; +import {firstValueFrom, forkJoin, Observable} from "rxjs"; import {SortDirection} from "@angular/material/sort"; -import {CompanyDto} from "../../models/dtos/CompanyDto"; import {PlayerPerspectiveDto} from "../../models/dtos/PlayerPerspectiveDto"; +import {ActivatedRoute, Params, Router} from "@angular/router"; +import {Location} from "@angular/common"; +import {HttpParams} from "@angular/common/http"; class SortOption { title: string; @@ -60,7 +62,10 @@ export class LibraryOverviewComponent implements AfterContentInit { loading: boolean = true; gameLibraryIsEmpty: boolean = false; - constructor(private gameServerService: GamesService) { + constructor(private gameServerService: GamesService, + private route: ActivatedRoute, + private router: Router, + private location: Location) { } ngAfterContentInit(): void { @@ -83,7 +88,16 @@ export class LibraryOverviewComponent implements AfterContentInit { this.availableThemes = result[1]; this.availablePlayerPerspectives = result[2]; - this.refreshLibraryView().then(() => this.loading = false); + this.route.queryParams.subscribe(params => { + if (params['search'] !== undefined) this.searchTerm = params['search']; + if (params['sort'] !== undefined) this.selectedSortOption = this.matchSelectedSortOptionFromParam(params['sort']); + if (params['gamemodes'] !== undefined) this.setSelectedGamemodesFromParam(params['gamemodes']); + if (params['genres'] !== undefined) this.activeGenreFilters = this.matchSelectedFilters(this.availableGenres, params['genres']); + if (params['themes'] !== undefined) this.activeThemeFilters = this.matchSelectedFilters(this.availableThemes, params['themes']); + if (params['playerPerspectives'] !== undefined) this.activePlayerPerspectiveFilters = this.matchSelectedFilters(this.availablePlayerPerspectives, params['playerPerspectives']); + + this.refreshLibraryView().then(() => this.loading = false); + }); }); } ); @@ -91,7 +105,8 @@ export class LibraryOverviewComponent implements AfterContentInit { async refreshLibraryView(): Promise { let games: DetectedGameDto[] = await firstValueFrom(this.gameServerService.getAllGames()); - this.games = this.sortGames(this.filterGames(games)); + this.games = this.sortGames(this.filterGames(games)); + this.saveStateToRoute(); } clearSearchTerm(): void { @@ -130,11 +145,11 @@ export class LibraryOverviewComponent implements AfterContentInit { // @ts-ignore let f2 = g2[this.selectedSortOption.field]; - if(f1 > f2) return 1; - if(f1 < f2) return -1; + if (f1 > f2) return 1; + if (f1 < f2) return -1; return 0; }); - if(this.selectedSortOption.direction === "desc") games = games.reverse(); + if (this.selectedSortOption.direction === "desc") games = games.reverse(); return games; } @@ -183,4 +198,49 @@ export class LibraryOverviewComponent implements AfterContentInit { this.refreshLibraryView(); } + private saveStateToRoute(): void { + let stateParams: Params = {}; + + if (this.searchTerm.trim().length > 0) stateParams['search'] = this.searchTerm; + if (this.selectedSortOption !== this.defaultSortOption) stateParams['sort'] = this.toParam(this.selectedSortOption); + if (this.getActiveGameModesFilters().length > 0) stateParams['gamemodes'] = this.getActiveGameModesFilters().join(','); + if (this.activeGenreFilters.length > 0) stateParams['genres'] = this.activeGenreFilters.join(','); + if (this.activeThemeFilters.length > 0) stateParams['themes'] = this.activeThemeFilters.join(','); + if (this.activePlayerPerspectiveFilters.length > 0) stateParams['playerPerspectives'] = this.activePlayerPerspectiveFilters.join(','); + + const url = this.router.createUrlTree([], {relativeTo: this.route, queryParams: stateParams}).toString(); + this.location.go(url); + } + + private toParam(sortOption: SortOption): string { + return `${sortOption.field}_${sortOption.direction}`; + } + + private matchSelectedSortOptionFromParam(sortParam: string): SortOption { + return this.sortOptions.find(s => sortParam === this.toParam(s)) ?? this.defaultSortOption; + } + + private matchSelectedFilters(options: any[], paramString: string): string[] { + let params: string[] = paramString.split(","); + return options.filter(o => params.includes(o.slug)).map(o => o.slug); + } + + private getActiveGameModesFilters(): string[] { + let activeFilters: string[] = []; + + if (this.offlineCoopFilterEnabled) activeFilters.push('offlineCoop'); + if (this.onlineCoopFilterEnabled) activeFilters.push('onlineCoop'); + if (this.lanSupportFilterEnabled) activeFilters.push('lanSupport'); + + return activeFilters; + } + + private setSelectedGamemodesFromParam(paramString: string): void { + let params: string[] = paramString.split(","); + + if (params.includes('offlineCoop')) this.offlineCoopFilterEnabled = true; + if (params.includes('onlineCoop')) this.onlineCoopFilterEnabled = true; + if (params.includes('lanSupport')) this.lanSupportFilterEnabled = true; + } + } From 291d7ad8d8aaf11bec98c5b4d815067cb6711eeb Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:30:04 +0200 Subject: [PATCH 13/16] Fixed "Go to library screen" shown on library screen when filters were active --- frontend/src/app/components/header/header.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts index 5c8c67f..2bf9562 100644 --- a/frontend/src/app/components/header/header.component.ts +++ b/frontend/src/app/components/header/header.component.ts @@ -51,7 +51,7 @@ export class HeaderComponent { } onLibraryScreen(): boolean { - return this.router.url === "/library"; + return this.router.url.startsWith("/library"); } onLibraryManagementScreen(): boolean { From e0550de34d83114e707a69e905a5b0c4fa983a31 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:32:09 +0200 Subject: [PATCH 14/16] Fixed bug in previous bugfix --- frontend/src/app/components/header/header.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts index 2bf9562..aafe9c6 100644 --- a/frontend/src/app/components/header/header.component.ts +++ b/frontend/src/app/components/header/header.component.ts @@ -51,7 +51,7 @@ export class HeaderComponent { } onLibraryScreen(): boolean { - return this.router.url.startsWith("/library"); + return this.router.url.startsWith("/library&") || this.router.url === "/library"; } onLibraryManagementScreen(): boolean { From d6a4a5f5d94ef1f144a89c2740c3c27900158a3d Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:45:19 +0200 Subject: [PATCH 15/16] Fixed bug in previous bugfix Part II - Electric Bogaloo --- frontend/src/app/components/header/header.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts index aafe9c6..71b017a 100644 --- a/frontend/src/app/components/header/header.component.ts +++ b/frontend/src/app/components/header/header.component.ts @@ -1,11 +1,11 @@ -import {Component, Inject} from '@angular/core'; +import {Component} from '@angular/core'; import {LibraryService} from "../../services/library.service"; import {MatSnackBar} from '@angular/material/snack-bar'; import {timeInterval} from "rxjs"; import {Router} from "@angular/router"; import {GamesService} from "../../services/games.service"; import {ThemingService} from "../../services/theming.service"; -import {DOCUMENT, Location} from '@angular/common'; +import {Location} from '@angular/common'; @Component({ selector: 'app-header', @@ -51,7 +51,7 @@ export class HeaderComponent { } onLibraryScreen(): boolean { - return this.router.url.startsWith("/library&") || this.router.url === "/library"; + return this.router.url.startsWith("/library?") || this.router.url === "/library"; } onLibraryManagementScreen(): boolean { From 0e9504b420766835eb565466ebdc639fbb139ecb Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:47:15 +0200 Subject: [PATCH 16/16] Release 1.1.0 --- backend/pom.xml | 4 ++-- frontend/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index c795310..a629033 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -7,7 +7,7 @@ gameyfin de.grimsi - 1.0.1 + 1.1.0 gameyfin-backend @@ -227,4 +227,4 @@ - \ No newline at end of file + diff --git a/frontend/pom.xml b/frontend/pom.xml index db25e89..f780850 100644 --- a/frontend/pom.xml +++ b/frontend/pom.xml @@ -5,7 +5,7 @@ gameyfin de.grimsi - 1.0.1 + 1.1.0 4.0.0 diff --git a/pom.xml b/pom.xml index dab9593..fd45415 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ de.grimsi gameyfin - 1.0.1 + 1.1.0 gameyfin gameyfin