diff --git a/migrations/Version20260722201120.php b/migrations/Version20260722201120.php new file mode 100644 index 0000000..b1feefe --- /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), UNIQUE INDEX unique_collection_type_item (collection, type, item), 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..1fe57de --- /dev/null +++ b/src/main/php/tracky/UserCollectionItemType.php @@ -0,0 +1,31 @@ + self::SHOW, + Season::class => self::SEASON, + Episode::class => self::EPISODE, + Movie::class => self::MOVIE, + MovieSet::class => self::MOVIE_SET + ]; + + return $typeMap[$entity::class] ?? null; + } +} diff --git a/src/main/php/tracky/controller/UserCollectionController.php b/src/main/php/tracky/controller/UserCollectionController.php new file mode 100644 index 0000000..4b9a4aa --- /dev/null +++ b/src/main/php/tracky/controller/UserCollectionController.php @@ -0,0 +1,291 @@ + + */ + private array $typeToRepositoryMap; + + public function __construct( + ShowRepository $showRepository, + SeasonRepository $seasonRepository, + EpisodeRepository $episodeRepository, + MovieRepository $movieRepository, + MovieSetRepository $movieSetRepository + ) + { + $this->typeToRepositoryMap = [ + UserCollectionItemType::SHOW->value => $showRepository, + UserCollectionItemType::SEASON->value => $seasonRepository, + UserCollectionItemType::EPISODE->value => $episodeRepository, + UserCollectionItemType::MOVIE->value => $movieRepository, + UserCollectionItemType::MOVIE_SET->value => $movieSetRepository + ]; + } + + #[Route("/users/{username}/collections", name: "user_profile_collections_page", methods: ["GET"])] + public function getListPage(User $user, UserCollectionRepository $userCollectionRepository): Response + { + return $this->render("user/collections/collections.twig", [ + "user" => $user, + "collections" => $userCollectionRepository->findBy(["user" => $user]) + ]); + } + + #[Route("/users/{username}/collections.json", name: "user_profile_collections_json", methods: ["GET"])] + public function getCollectionsJson(User $user, UserCollectionRepository $userCollectionRepository): Response + { + $data = []; + + foreach ($userCollectionRepository->findBy(["user" => $user]) as $collection) { + $data[$collection->getId()] = $collection->getName(); + } + + return $this->json($data); + } + + #[Route("/users/{username}/collections/{collection}", name: "user_profile_collection_page", methods: ["GET"])] + public function getCollectionPage(User $user, UserCollection $collection, Request $request): Response + { + $sort = explode("/", trim($request->query->get("sort", "")), 2); + + list($sortBy, $sortDirection) = $sort + ["", ""]; + + $sortOptions = ["title", "type", "added", "runtime"]; + + if (!in_array($sortBy, $sortOptions)) { + $sortBy = "added"; + } + + if (!in_array($sortDirection, ["asc", "desc"])) { + $sortDirection = "asc"; + } + + /** + * @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 ($this->typeToRepositoryMap[$type]->findByIds($itemIds) as $resolvedItem) { + $index = array_search($resolvedItem->getId(), $itemIds); + $items[$index]->setResolvedItem($resolvedItem); + } + } + + $items = iterator_to_array($collection->getItems()); + + usort($items, function(UserCollectionItem $item1, UserCollectionItem $item2) use ($sortBy, $sortDirection) { + $resolvedItem1 = $item1->getResolvedItem(); + $resolvedItem2 = $item2->getResolvedItem(); + + switch ($sortBy) { + case "title": + $value1 = $resolvedItem1->getTitle(); + $value2 = $resolvedItem2->getTitle(); + break; + case "type": + $value1 = $item1->getType(); + $value2 = $item2->getType(); + break; + case "added": + $value1 = $item1->getAddedAt()->getTimestamp(); + $value2 = $item2->getAddedAt()->getTimestamp(); + break; + case "runtime": + $value1 = $resolvedItem1->getRuntime(); + $value2 = $resolvedItem2->getRuntime(); + break; + default: + return 0; + } + + if ($value1 < $value2) { + return $sortDirection == "asc" ? -1 : 1; + } elseif ($value1 > $value2) { + return $sortDirection == "asc" ? 1 : -1; + } else { + return 0; + } + }); + + return $this->render("user/collections/collection.twig", [ + "user" => $user, + "collection" => $collection, + "items" => $items, + "sortOptions" => $sortOptions, + "sort" => [ + "field" => $sortBy, + "direction" => $sortDirection + ] + ]); + } + + #[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 $entityManager): 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", "action" => "create", "error" => "empty-name", "name" => $name]); + } + + if ($userCollectionRepository->count(["user" => $user, "name" => $name])) { + return $this->redirectToRoute("user_profile_collections_page", ["username" => $user->getUsername(), "flash" => "error", "action" => "create", "error" => "duplicate-collection", "name" => $name]); + } + + $collection = new UserCollection; + $collection->setUser($user); + $collection->setName($name); + $collection->setCreatedAt(new DateTime); + + $entityManager->persist($collection); + $entityManager->flush(); + + return $this->redirectToRoute("user_profile_collections_page", ["username" => $user->getUsername(), "flash" => "success", "action" => "create", "name" => $name]); + } + + #[Route("/users/{username}/collections/{collection}", name: "user_profile_collection_remove_action", methods: ["DELETE"])] + #[IsGranted("IS_AUTHENTICATED")] + public function removeCollection(User $user, UserCollection $collection, EntityManagerInterface $entityManager): Response + { + /** + * @var User + */ + $currentUser = $this->getUser(); + + if ($collection->getUser()->getId() !== $user->getId() or $user->getId() !== $currentUser->getId()) { + throw new AccessDeniedHttpException; + } + + $entityManager->remove($collection); + $entityManager->flush(); + + return new Response("Collection removed"); + } + + #[Route("/users/{username}/collections/{collection}/rename", name: "user_profile_collection_rename_action", methods: ["POST"])] + #[IsGranted("IS_AUTHENTICATED")] + public function renameCollection(User $user, UserCollection $collection, Request $request, UserCollectionRepository $userCollectionRepository, EntityManagerInterface $entityManager): Response + { + /** + * @var User + */ + $currentUser = $this->getUser(); + + if ($collection->getUser()->getId() !== $user->getId() or $user->getId() !== $currentUser->getId()) { + throw new AccessDeniedHttpException; + } + + $name = trim($request->request->getString("name")); + if ($name === "") { + return $this->redirectToRoute("user_profile_collection_page", ["username" => $user->getUsername(), "collection" => $collection->getId(), "flash" => "error", "error" => "empty-name"]); + } + + foreach ($userCollectionRepository->findBy(["user" => $user, "name" => $name]) as $otherCollection) { + if ($otherCollection->getId() !== $collection->getId()) { + return $this->redirectToRoute("user_profile_collection_page", ["username" => $user->getUsername(), "collection" => $collection->getId(), "flash" => "error", "error" => "duplicate-collection", "name" => $name]); + } + } + + $oldName = $collection->getName(); + $collection->setName($name); + + $entityManager->persist($collection); + $entityManager->flush(); + + return $this->redirectToRoute("user_profile_collection_page", ["username" => $user->getUsername(), "collection" => $collection->getId(), "flash" => "success", "name" => $oldName, "new-name" => $name]); + } + + #[Route("/users/{username}/collections/{collection}/add-item", name: "user_profile_collection_add_item_action", methods: ["POST"])] + #[IsGranted("IS_AUTHENTICATED")] + public function addItemToCollection(User $user, UserCollection $collection, Request $request, UserCollectionItemRepository $userCollectionItemRepository, EntityManagerInterface $entityManager): Response + { + /** + * @var User + */ + $currentUser = $this->getUser(); + + if ($collection->getUser()->getId() !== $user->getId() or $user->getId() !== $currentUser->getId()) { + throw new AccessDeniedHttpException; + } + + $payload = $request->getPayload(); + + $type = UserCollectionItemType::tryFrom($payload->getString("type")); + if ($type === null) { + throw new BadRequestHttpException("Invalid type!"); + } + + $item = $this->typeToRepositoryMap[$type->value]->findOneBy(["id" => $payload->getInt("item")]); + if ($item === null) { + throw new NotFoundHttpException("Item not found!"); + } + + if ($userCollectionItemRepository->count(["collection" => $collection->getId(), "type" => $type->value, "item" => $item->getId()])) { + throw new UnprocessableEntityHttpException("Item already exists in collection!"); + } + + $collectionItem = new UserCollectionItem; + $collectionItem->setCollection($collection); + $collectionItem->setItem($item); + $collectionItem->setAddedAt(new DateTime); + + $entityManager->persist($collectionItem); + $entityManager->flush(); + + return new Response("Item added"); + } +} diff --git a/src/main/php/tracky/model/Season.php b/src/main/php/tracky/model/Season.php index e52a2f7..905f1be 100644 --- a/src/main/php/tracky/model/Season.php +++ b/src/main/php/tracky/model/Season.php @@ -137,20 +137,15 @@ public function fetchPosterImages(ImageFetcher $imageFetcher, bool $includeEpiso } } - public function getTotalRuntime(): int + public function getRuntime(): int { - $totalRuntime = 0; + $runtime = 0; foreach ($this->getEpisodes() as $episode) { - $runtime = $episode->getRuntime(); - if ($runtime === null) { - continue; - } - - $totalRuntime += $runtime; + $runtime += $episode->getRuntime(); } - return $totalRuntime; + return $runtime; } public function getFirstAired(): ?Date diff --git a/src/main/php/tracky/model/Show.php b/src/main/php/tracky/model/Show.php index 84e3128..a1b07cc 100644 --- a/src/main/php/tracky/model/Show.php +++ b/src/main/php/tracky/model/Show.php @@ -189,6 +189,17 @@ public function getRandomEpisodes(int $count): array return $randomEpisodes; } + public function getRuntime(): int + { + $runtime = 0; + + foreach ($this->getSeasons() as $season) { + $runtime += $season->getRuntime(); + } + + return $runtime; + } + /** * @return list */ diff --git a/src/main/php/tracky/model/UserCollection.php b/src/main/php/tracky/model/UserCollection.php new file mode 100644 index 0000000..8da6992 --- /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..d706ef1 --- /dev/null +++ b/src/main/php/tracky/model/UserCollectionItem.php @@ -0,0 +1,86 @@ +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->type = UserCollectionItemType::fromEntity($item); + $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(): Show|Season|Episode|Movie|MovieSet + { + 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/model/traits/Runtime.php b/src/main/php/tracky/model/traits/Runtime.php index 52af9b8..dc3e1c0 100644 --- a/src/main/php/tracky/model/traits/Runtime.php +++ b/src/main/php/tracky/model/traits/Runtime.php @@ -8,9 +8,9 @@ trait Runtime #[ORM\Column(type: "integer", nullable: true)] private ?int $runtime; - public function getRuntime(): ?int + public function getRuntime(): int { - return $this->runtime; + return $this->runtime ?? 0; } public function setRuntime(?int $runtime): self 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 @@ + { + let username = document.querySelector('meta[name="username"]')?.content; + let addCollectionItemTooltipElement = document.querySelector("#add-collection-item-tooltip")!; + let removeCollectionTooltipElement = document.querySelector("#remove-collection-tooltip")!; + let collections: Dictionary | null = null; + let activeAddCollectionItemEntry: DOMStringMap | null = null; + let activeRemoveCollectionEntry: DOMStringMap | null = null; + + function loadCollections() { + if (collections === null) { + fetch(`/users/${username}/collections.json`) + .then((response) => response.json()) + .then((json) => { + collections = json as Dictionary; + + let selectElement = document.querySelector("#add-collection-item-tooltip-collection"); + + Object.entries(collections).forEach(([id, name]) => { + let optionElement = document.createElement("option"); + optionElement.value = id; + optionElement.textContent = name; + selectElement?.add(optionElement); + }); + }); + } + } + + document.querySelectorAll(".add-to-collection").forEach((buttonElement) => { + buttonElement.addEventListener("click", () => { + loadCollections(); + + createPopper(buttonElement, addCollectionItemTooltipElement, { + placement: "bottom" + }); + + addCollectionItemTooltipElement.style.display = "block"; + activeAddCollectionItemEntry = buttonElement.dataset; + }); + }); + + document.querySelector("#add-collection-item-tooltip-form")?.addEventListener("submit", (event) => { + event.preventDefault(); + + if (activeAddCollectionItemEntry === null) { + return; + } + + let selectElement = document.querySelector("#add-collection-item-tooltip-collection"); + let collectionId = selectElement?.selectedOptions.item(0)?.value ?? null; + + if (collectionId === null) { + return; + } + + fetch(`/users/${username}/collections/${collectionId}/add-item`, { + method: "POST", + body: JSON.stringify({ + type: activeAddCollectionItemEntry.type, + item: activeAddCollectionItemEntry.item + }) + }); + + // Hide the modal + addCollectionItemTooltipElement.style.display = "none"; + activeAddCollectionItemEntry = null; + }); + + document.querySelector("#add-collection-item-tooltip-cancel")?.addEventListener("click", () => { + addCollectionItemTooltipElement.style.display = "none"; + activeAddCollectionItemEntry = null; + }); + + document.querySelector("#rename-collection")?.addEventListener("click", () => { + document.querySelectorAll("#collection-name, #rename-collection").forEach((element) => element.classList.add("d-none")); + document.querySelector("#collection-rename-form")?.classList.remove("d-none"); + }); + + document.querySelector("#collection-rename-cancel")?.addEventListener("click", () => { + document.querySelectorAll("#collection-name, #rename-collection").forEach((element) => element.classList.remove("d-none")); + + let form = document.querySelector("#collection-rename-form"); + form?.classList.add("d-none"); + form?.reset(); + }); + + document.querySelector("#remove-collection")?.addEventListener("click", () => { + let buttonElement = document.querySelector("#remove-collection")!; + + createPopper(buttonElement, removeCollectionTooltipElement, { + placement: "bottom" + }); + + removeCollectionTooltipElement.style.display = "block"; + activeRemoveCollectionEntry = buttonElement.dataset; + }); + + document.querySelector("#remove-collection-tooltip-confirm")?.addEventListener("click", () => { + if (activeRemoveCollectionEntry === null) { + return; + } + + let collectionName = activeRemoveCollectionEntry.name; + + fetch(`/users/${username}/collections/${activeRemoveCollectionEntry.id}`, { + method: "DELETE" + }).then(() => { + document.location.href = `/users/${username}/collections?flash=success&action=remove&name=${encodeURIComponent(collectionName ?? "")}`; + }); + }); + + document.querySelector("#remove-collection-tooltip-cancel")?.addEventListener("click", () => { + removeCollectionTooltipElement.style.display = "none"; + activeRemoveCollectionEntry = null; + }); +}); diff --git a/src/main/resources/lang/messages+intl-icu.de.yaml b/src/main/resources/lang/messages+intl-icu.de.yaml index fd36f2d..4c10311 100644 --- a/src/main/resources/lang/messages+intl-icu.de.yaml +++ b/src/main/resources/lang/messages+intl-icu.de.yaml @@ -68,6 +68,14 @@ user: logout: "Logout" login: "Login" no-history-available: "Kein Verlauf verfügbar." + collections: + title: "Sammlungen" + create-collection: "Sammlung erstellen" + no-collections-available: "Keine Sammlungen verfügbar." + no-items-available: "Keine Elemente verfügbar." + name: "Name" + items: "Elemente" + creation-date: "Erstelldatum" search: title: "Suche" diff --git a/src/main/resources/lang/messages+intl-icu.en.yaml b/src/main/resources/lang/messages+intl-icu.en.yaml index 5597332..157632a 100644 --- a/src/main/resources/lang/messages+intl-icu.en.yaml +++ b/src/main/resources/lang/messages+intl-icu.en.yaml @@ -66,6 +66,46 @@ user: logout: "Logout" login: "Login" no-history-available: "No history available." + collections: + title: "Collections" + create: + title: "Create collection" + submit-response: + success: "Collection ''{name}'' created." + error: + empty-name: "No name specified!" + duplicate-collection: "Another collection with the name ''{name}'' already exists!" + rename: + title: "Rename" + submit-response: + success: "Collection ''{name}'' renamed to ''{newName}''." + error: + empty-name: "No name specified!" + duplicate-collection: "Another collection with the name ''{name}'' already exists!" + ok: "OK" + cancel: "Cancel" + remove: + title: "Remove" + header: "Remove collection" + ok: "OK" + cancel: "Cancel" + submit-response: + success: "Collection ''{name}'' removed." + add-item: + header: "Add item to collection" + ok: "OK" + cancel: "Cancel" + add-to-collection: "Add to collection" + no-collections-available: "No collections available." + no-items-available: "No items available." + name: "Name" + items: "Items" + creation-date: "Creation date" + sort: + title: "Title" + type: "Type" + added: "Added" + runtime: "Runtime" search: title: "Search" diff --git a/templates/components/macros.twig b/templates/components/macros.twig index 7aeeb19..7052093 100644 --- a/templates/components/macros.twig +++ b/templates/components/macros.twig @@ -62,3 +62,21 @@ {{ ((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/components/season-card-small.twig b/templates/components/season-card-small.twig index 407f48f..478a476 100644 --- a/templates/components/season-card-small.twig +++ b/templates/components/season-card-small.twig @@ -11,7 +11,7 @@ {{ season.year }} {{ "shows.count.episodes"|trans({"%count%": season.episodes|length}) }} - {{ "runtime"|trans({"%runtime%": season.totalRuntime}) }} + {{ "runtime"|trans({"%runtime%": season.runtime}) }} {% if app.user %} diff --git a/templates/components/view-buttons.twig b/templates/components/view-buttons.twig index 024898f..9891890 100644 --- a/templates/components/view-buttons.twig +++ b/templates/components/view-buttons.twig @@ -5,6 +5,7 @@ {% set attributes = {"data-show-id": item.season.show.id, "data-season": item.season.number, "data-episode": item.number} %} {% endif %}
+
diff --git a/templates/movieset.twig b/templates/movieset.twig index 0373509..72e80c7 100644 --- a/templates/movieset.twig +++ b/templates/movieset.twig @@ -9,33 +9,43 @@
-

{{ movieSet.title }}

+
+
+

{{ movieSet.title }}

-
- {% if movieSet.tmdbId %} - TMDB - {% endif %} - {% if movieSet.tvdbId %} - TVDB - {% endif %} -
+
+ {% if movieSet.tmdbId %} + TMDB + {% endif %} + {% if movieSet.tvdbId %} + TVDB + {% endif %} +
-
- {{ "movies.count"|trans({"%count%": movieSet.movies|length}) }} +
+ {{ "movies.count"|trans({"%count%": movieSet.movies|length}) }} - {% set firstMovie = movieSet.movies.first %} - {% set lastMovie = movieSet.movies.last %} + {% set firstMovie = movieSet.movies.first %} + {% set lastMovie = movieSet.movies.last %} - {% if firstMovie.year == lastMovie.year %} - {% set year = firstMovie.year %} - {% else %} - {% set year = firstMovie.year ~ " - " ~ lastMovie.year %} - {% endif %} + {% if firstMovie.year == lastMovie.year %} + {% set year = firstMovie.year %} + {% else %} + {% set year = firstMovie.year ~ " - " ~ lastMovie.year %} + {% endif %} + + {{ year }} - {{ year }} + {% if movieSet.runtime %} + {{ "runtime"|trans({"%runtime%": movieSet.runtime}) }} + {% endif %} +
+
- {% if movieSet.runtime %} - {{ "runtime"|trans({"%runtime%": movieSet.runtime}) }} + {% if is_granted("IS_AUTHENTICATED") %} +
+ +
{% endif %}
diff --git a/templates/page.twig b/templates/page.twig index 33e8c8b..4aa15df 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 %} @@ -17,6 +6,10 @@ + {% if app.user %} + + {% endif %} + {% set pageTitle = block("title") %} {% if pageTitle is not empty %}{{ pageTitle|raw }} - {% endif %}Tracky @@ -53,11 +46,11 @@
@@ -67,9 +60,10 @@ {% set profile_page_parameters = {"username": app.user.username} %} {% endif %}
@@ -77,15 +71,15 @@
@@ -144,6 +138,33 @@
+
+
+ +
+
{{ "user.collections.add-item.header"|trans }}
+
+ +
+ + +
+
+
+
+ +
+
+ +
+
{{ "user.collections.remove.header"|trans }}
+
+ + +
+
+
+ {% block additional_content %}{% endblock %} diff --git a/templates/shows/season.twig b/templates/shows/season.twig index b8f056b..29480ef 100644 --- a/templates/shows/season.twig +++ b/templates/shows/season.twig @@ -32,7 +32,7 @@ {{ season.year }} - {{ "runtime"|trans({"%runtime%": season.totalRuntime}) }} + {{ "runtime"|trans({"%runtime%": season.runtime}) }} {% if season.plot %} {{ season.plot }} @@ -44,7 +44,16 @@

{{ "shows.episodes"|trans }}

{% if is_granted("IS_AUTHENTICATED") and season.episodes|length > 0 %} + + + + {% endif %} + {% if show.plot %} {{ show.plot }} diff --git a/templates/user/collections/collection.twig b/templates/user/collections/collection.twig new file mode 100644 index 0000000..e85740c --- /dev/null +++ b/templates/user/collections/collection.twig @@ -0,0 +1,82 @@ +{% extends "user/page.twig" %} + +{% block title %}{{ collection.name }} - {{ user.username }}{% endblock %} + +{% block profileContent %} +
+
+
+

{{ collection.name }}

+
+
+ + + +
+
+
+ + {% if app.user == collection.user %} + + + {% endif %} +
+ +
+ +
+
+ + {% if user == app.user %} + {% if app.request.query.get("flash") == "success" %} +
{{ "user.collections.rename.submit-response.success"|trans({"name": app.request.query.get("name"), "newName": app.request.query.get("new-name")}) }}
+ {% elseif app.request.query.get("flash") == "error" %} +
{{ ("user.collections.rename.submit-response.error." ~ app.request.query.get("error"))|trans({"name": app.request.query.get("name")}) }}
+ {% endif %} + {% endif %} + + {% if items|length %} + {% set UserCollectionItemType = enum("tracky\\UserCollectionItemType") %} + +
+ {% for item in 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..12f466b --- /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." ~ app.request.query.get("action") ~ ".submit-response.success")|trans({"name": app.request.query.get("name")}) }}
+ {% elseif app.request.query.get("flash") == "error" %} +
{{ ("user.collections." ~ app.request.query.get("action") ~ ".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 %}