diff --git a/migrations/Version20260722201120.php b/migrations/Version20260722201120.php new file mode 100644 index 0000000..f5a63ad --- /dev/null +++ b/migrations/Version20260722201120.php @@ -0,0 +1,29 @@ +addSql('CREATE TABLE usercollectionitems (id INT AUTO_INCREMENT NOT NULL, collection INT NOT NULL, type ENUM(\'show\', \'season\', \'episode\', \'movie\', \'movieset\') NOT NULL, item INT NOT NULL, addedAt DATETIME NOT NULL, INDEX IDX_41B0376EFC4D6532 (collection), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE usercollections (id INT AUTO_INCREMENT NOT NULL, user INT NOT NULL, name VARCHAR(255) NOT NULL, createdAt DATETIME NOT NULL, INDEX IDX_1709E57A8D93D649 (user), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE usercollectionitems ADD CONSTRAINT FK_41B0376EFC4D6532 FOREIGN KEY (collection) REFERENCES usercollections (id)'); + $this->addSql('ALTER TABLE usercollections ADD CONSTRAINT FK_1709E57A8D93D649 FOREIGN KEY (user) REFERENCES users (id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE usercollectionitems DROP FOREIGN KEY FK_41B0376EFC4D6532'); + $this->addSql('ALTER TABLE usercollections DROP FOREIGN KEY FK_1709E57A8D93D649'); + $this->addSql('DROP TABLE usercollectionitems'); + $this->addSql('DROP TABLE usercollections'); + } +} diff --git a/src/main/php/tracky/UserCollectionItemType.php b/src/main/php/tracky/UserCollectionItemType.php new file mode 100644 index 0000000..ba2ae85 --- /dev/null +++ b/src/main/php/tracky/UserCollectionItemType.php @@ -0,0 +1,11 @@ +render("user/collections/collections.twig", [ + "user" => $user, + "collections" => $userCollectionRepository->findBy(["user" => $user]) + ]); + } + + #[Route("/users/{username}/collections/{collection}", name: "user_profile_collection_page", methods: ["GET"])] + public function getCollectionPage( + User $user, + UserCollection $collection, + ShowRepository $showRepository, + SeasonRepository $seasonRepository, + EpisodeRepository $episodeRepository, + MovieRepository $movieRepository, + MovieSetRepository $movieSetRepository + ): Response + { + $typeToRepositoryMap = [ + UserCollectionItemType::SHOW->value => $showRepository, + UserCollectionItemType::SEASON->value => $seasonRepository, + UserCollectionItemType::EPISODE->value => $episodeRepository, + UserCollectionItemType::MOVIE->value => $movieRepository, + UserCollectionItemType::MOVIE_SET->value => $movieSetRepository + ]; + + /** + * @var array> + */ + $perTypeItems = []; + + foreach ($collection->getItems() as $item) { + $type = $item->getType()->value; + + if (!isset($perTypeItems[$type])) { + $perTypeItems[$type] = []; + } + + $perTypeItems[$type][$item->getId()] = $item; + } + + foreach ($perTypeItems as $type => $items) { + $itemIds = array_map(fn($item) => $item->getItem(), $items); + + /** + * @var BaseEntity $resolvedItem + */ + foreach ($typeToRepositoryMap[$type]->findByIds($itemIds) as $resolvedItem) { + $index = array_search($resolvedItem->getId(), $itemIds); + $items[$index]->setResolvedItem($resolvedItem); + } + } + + return $this->render("user/collections/collection.twig", [ + "user" => $user, + "collection" => $collection + ]); + } + + #[Route("/users/{username}/collections", name: "user_profile_collection_create_action", methods: ["POST"])] + #[IsGranted("IS_AUTHENTICATED")] + public function createCollection(User $user, Request $request, UserCollectionRepository $userCollectionRepository, EntityManagerInterface $entityManagerInterface): Response + { + /** + * @var User + */ + $currentUser = $this->getUser(); + + if ($user->getId() !== $currentUser->getId()) { + throw new AccessDeniedHttpException; + } + + $name = trim($request->request->getString("name")); + + if ($name === "") { + return $this->redirectToRoute("user_profile_collections_page", ["username" => $user->getUsername(), "flash" => "error", "error" => "empty-name", "name" => $name]); + } + + if ($userCollectionRepository->count(["user" => $user, "name" => $name])) { + return $this->redirectToRoute("user_profile_collections_page", ["username" => $user->getUsername(), "flash" => "error", "error" => "duplicate-collection", "name" => $name]); + } + + $collection = new UserCollection; + $collection->setUser($user); + $collection->setName($name); + $collection->setCreatedAt(new DateTime); + + $entityManagerInterface->persist($collection); + $entityManagerInterface->flush(); + + return $this->redirectToRoute("user_profile_collections_page", ["username" => $user->getUsername(), "flash" => "success", "name" => $name]); + } +} diff --git a/src/main/php/tracky/model/UserCollection.php b/src/main/php/tracky/model/UserCollection.php new file mode 100644 index 0000000..fce803c --- /dev/null +++ b/src/main/php/tracky/model/UserCollection.php @@ -0,0 +1,77 @@ +user; + } + + public function setUser(User $user): self + { + $this->user = $user; + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + return $this; + } + + public function getCreatedAt(): DateTime + { + return $this->createdAt; + } + + public function setCreatedAt(DateTime $date): self + { + $this->createdAt = $date; + return $this; + } + + /** + * @return UserCollectionItem[] + */ + public function getItems(): mixed + { + return $this->items; + } + + public function setItems(mixed $items): self + { + $this->items = $items; + return $this; + } + + public function addItem(UserCollectionItem $item): self + { + $this->items[] = $item; + return $this; + } +} diff --git a/src/main/php/tracky/model/UserCollectionItem.php b/src/main/php/tracky/model/UserCollectionItem.php new file mode 100644 index 0000000..f2f8221 --- /dev/null +++ b/src/main/php/tracky/model/UserCollectionItem.php @@ -0,0 +1,91 @@ +collection; + } + + public function setCollection(UserCollection $collection): self + { + $this->collection = $collection; + return $this; + } + + public function getType(): UserCollectionItemType + { + return $this->type; + } + + public function setType(UserCollectionItemType $type): self + { + $this->type = $type; + return $this; + } + + public function getItem(): int + { + return $this->item; + } + + public function setItem(Show|Season|Episode|Movie|MovieSet $item): self + { + $this->item = $item->getId(); + $this->resolvedItem = $item; + return $this; + } + + public function getAddedAt(): DateTime + { + return $this->addedAt; + } + + public function setAddedAt(DateTime $date): self + { + $this->addedAt = $date; + return $this; + } + + public function getResolvedItem(): BaseEntity + { + return $this->resolvedItem; + } + + public function setResolvedItem(Show|Season|Episode|Movie|MovieSet $item): self + { + $this->resolvedItem = $item; + return $this; + } +} diff --git a/src/main/php/tracky/orm/AbstractRepository.php b/src/main/php/tracky/orm/AbstractRepository.php new file mode 100644 index 0000000..9c878ef --- /dev/null +++ b/src/main/php/tracky/orm/AbstractRepository.php @@ -0,0 +1,20 @@ +createQueryBuilder("entity"); + + $queryBuilder->where($queryBuilder->expr()->in("entity.id", $ids)); + + return $queryBuilder->getQuery()->getResult(); + } +} diff --git a/src/main/php/tracky/orm/EpisodeRepository.php b/src/main/php/tracky/orm/EpisodeRepository.php index a804c61..c1adc87 100644 --- a/src/main/php/tracky/orm/EpisodeRepository.php +++ b/src/main/php/tracky/orm/EpisodeRepository.php @@ -1,11 +1,10 @@ getQuery() ->getResult(); } - - /** - * @return Episode[] - */ - public function findByIds(array $ids): array - { - if (empty($ids)) { - return []; - } - - $queryBuilder = $this->createQueryBuilder("episode"); - - $queryBuilder->where($queryBuilder->expr()->in("episode.id", $ids)); - - return $queryBuilder->getQuery()->getResult(); - } } diff --git a/src/main/php/tracky/orm/MovieRepository.php b/src/main/php/tracky/orm/MovieRepository.php index 71608d3..702f742 100644 --- a/src/main/php/tracky/orm/MovieRepository.php +++ b/src/main/php/tracky/orm/MovieRepository.php @@ -1,11 +1,10 @@ createQueryBuilder("movie"); - - $queryBuilder->where($queryBuilder->expr()->in("movie.id", $ids)); - - return $queryBuilder->getQuery()->getResult(); - } } diff --git a/src/main/php/tracky/orm/MovieSetRepository.php b/src/main/php/tracky/orm/MovieSetRepository.php index 86deb65..13b485a 100644 --- a/src/main/php/tracky/orm/MovieSetRepository.php +++ b/src/main/php/tracky/orm/MovieSetRepository.php @@ -1,12 +1,11 @@ getResult(); } - - /** - * @return MovieSet[] - */ - public function findByIds(array $ids): array - { - if (empty($ids)) { - return []; - } - - $queryBuilder = $this->createQueryBuilder("movieset"); - - $queryBuilder->where($queryBuilder->expr()->in("movieset.id", $ids)); - - return $queryBuilder->getQuery()->getResult(); - } } diff --git a/src/main/php/tracky/orm/ScrobbleQueueItemRepository.php b/src/main/php/tracky/orm/ScrobbleQueueItemRepository.php index a15affb..b54a654 100644 --- a/src/main/php/tracky/orm/ScrobbleQueueItemRepository.php +++ b/src/main/php/tracky/orm/ScrobbleQueueItemRepository.php @@ -1,11 +1,10 @@ getOneOrNullResult(); } - - /** - * @return Show[] - */ - public function findByIds(array $ids): array - { - if (empty($ids)) { - return []; - } - - $queryBuilder = $this->createQueryBuilder("show"); - - $queryBuilder->where($queryBuilder->expr()->in("show.id", $ids)); - - return $queryBuilder->getQuery()->getResult(); - } } diff --git a/src/main/php/tracky/orm/UserCollectionItemRepository.php b/src/main/php/tracky/orm/UserCollectionItemRepository.php new file mode 100644 index 0000000..4c6b923 --- /dev/null +++ b/src/main/php/tracky/orm/UserCollectionItemRepository.php @@ -0,0 +1,13 @@ +{{ ((watchedEpisodes / episodes|length) * 100)|round }}% {% endmacro %} + +{% macro navItem(path, title, icon_classes = null, route_starts_with = null, parameters = {}) %} + {% if route_starts_with != null and app.request.attributes.get("_route") starts with route_starts_with %} + {% set active = true %} + {% elseif app.request.attributes.get("_route") == path %} + {% set active = true %} + {% else %} + {% set active = false %} + {% endif %} + +{% endmacro %} diff --git a/templates/page.twig b/templates/page.twig index 33e8c8b..5b1c102 100644 --- a/templates/page.twig +++ b/templates/page.twig @@ -1,15 +1,4 @@ -{% macro navItem(icon_classes, path, title, route_starts_with = null, parameters = {}) %} - {% if route_starts_with != null and app.request.attributes.get("_route") starts with route_starts_with %} - {% set active = true %} - {% elseif app.request.attributes.get("_route") == path %} - {% set active = true %} - {% else %} - {% set active = false %} - {% endif %} - -{% endmacro %} +{% from "components/macros.twig" import navItem %} @@ -53,11 +42,11 @@
@@ -67,9 +56,10 @@ {% set profile_page_parameters = {"username": app.user.username} %} {% endif %}
@@ -77,15 +67,15 @@
diff --git a/templates/user/collections/collection.twig b/templates/user/collections/collection.twig new file mode 100644 index 0000000..1d35c2b --- /dev/null +++ b/templates/user/collections/collection.twig @@ -0,0 +1,31 @@ +{% extends "user/page.twig" %} + +{% block title %}{{ collection.name }} - {{ user.username }}{% endblock %} + +{% block profileContent %} +

{{ collection.name }}

+ + {% if collection.items|length %} + {% set UserCollectionItemType = enum("tracky\\UserCollectionItemType") %} + +
+ {% for item in collection.items %} +
+ {% if item.type == UserCollectionItemType.SHOW %} + {% include "components/show-card-small.twig" with {"show": item.resolvedItem} %} + {% elseif item.type == UserCollectionItemType.SEASON %} + {% include "components/season-card-small.twig" with {"season": item.resolvedItem} %} + {% elseif item.type == UserCollectionItemType.EPISODE %} + {% include "components/episode-card-small.twig" with {"episode": item.resolvedItem} %} + {% elseif item.type == UserCollectionItemType.MOVIE %} + {% include "components/movie-card-small.twig" with {"movie": item.resolvedItem} %} + {% elseif item.type == UserCollectionItemType.MOVIE_SET %} + {% include "components/movieset-card-small.twig" with {"movieSet": item.resolvedItem} %} + {% endif %} +
+ {% endfor %} +
+ {% else %} +
{{ "user.collections.no-items-available"|trans }}
+ {% endif %} +{% endblock %} diff --git a/templates/user/collections/collections.twig b/templates/user/collections/collections.twig new file mode 100644 index 0000000..ef1588e --- /dev/null +++ b/templates/user/collections/collections.twig @@ -0,0 +1,47 @@ +{% extends "user/page.twig" %} + +{% block title %}{{ "user.collections.title"|trans }} - {{ user.username }}{% endblock %} + +{% block profileContent %} +

{{ "user.collections.title"|trans }}

+ + {% if user == app.user %} + {% if app.request.query.get("flash") == "success" %} +
{{ "user.collections.create.submit-response.success"|trans({"name": app.request.query.get("name")}) }}
+ {% elseif app.request.query.get("flash") == "error" %} +
{{ ("user.collections.create.submit-response.error." ~ app.request.query.get("error"))|trans({"name": app.request.query.get("name")}) }}
+ {% endif %} + +
+
+ +
+ +
+ {% endif %} + + {% if collections %} + + + + + + + + + + + {% for collection in collections %} + + + + + + + {% endfor %} + +
#{{ "user.collections.name"|trans }}{{ "user.collections.items"|trans }}{{ "user.collections.creation-date"|trans }}
{{ loop.index }}{{ collection.name }}{{ collection.items|length }}{{ collection.createdAt.formatForDisplay }}
+ {% else %} +
{{ "user.collections.no-collections-available"|trans }}
+ {% endif %} +{% endblock %} diff --git a/templates/user/page.twig b/templates/user/page.twig index 7a7e5fa..7474310 100644 --- a/templates/user/page.twig +++ b/templates/user/page.twig @@ -1,8 +1,17 @@ +{% from "components/macros.twig" import navItem %} + {% extends "page.twig" %} -{% macro navItem(path, parameters, title) %} +{% macro navItem(path, title, route_starts_with = null, parameters = {}) %} + {% if route_starts_with != null and app.request.attributes.get("_route") starts with route_starts_with %} + {% set active = true %} + {% elseif app.request.attributes.get("_route") == path %} + {% set active = true %} + {% else %} + {% set active = false %} + {% endif %} {% endmacro %} @@ -10,9 +19,10 @@

{{ user.username }}

{% block profileContent %}{% endblock %}