Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eef567974 | |||
| 070723581a | |||
| f3a5c2012e | |||
| 5581a82554 | |||
| 3703272f59 | |||
| b587302b30 |
1
assets/icons/lets-icons/calendar-add-light.svg
Normal file
1
assets/icons/lets-icons/calendar-add-light.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor"><path d="M19.5 9.5v-.8c0-1.12 0-1.68-.218-2.108a2 2 0 0 0-.874-.874C17.98 5.5 17.42 5.5 16.3 5.5H7.7c-1.12 0-1.68 0-2.108.218a2 2 0 0 0-.874.874C4.5 7.02 4.5 7.58 4.5 8.7v.8m15 0v6.8c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874c-.428.218-.988.218-2.108.218H7.7c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C4.5 17.98 4.5 17.42 4.5 16.3V9.5m15 0h-15"/><path stroke-linecap="round" d="M8.5 3.5v4m7-4v4M12 17v-5m2.5 2.5h-5"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 529 B |
@@ -27,6 +27,7 @@
|
|||||||
"php-tmdb/api": "^4.1",
|
"php-tmdb/api": "^4.1",
|
||||||
"predis/predis": "^2.4",
|
"predis/predis": "^2.4",
|
||||||
"runtime/frankenphp-symfony": "^0.2.0",
|
"runtime/frankenphp-symfony": "^0.2.0",
|
||||||
|
"spatie/icalendar-generator": "^3.0",
|
||||||
"spomky-labs/pwa-bundle": "^1.2",
|
"spomky-labs/pwa-bundle": "^1.2",
|
||||||
"stof/doctrine-extensions-bundle": "^1.14",
|
"stof/doctrine-extensions-bundle": "^1.14",
|
||||||
"symfony/asset": "7.3.*",
|
"symfony/asset": "7.3.*",
|
||||||
|
|||||||
61
composer.lock
generated
61
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "6f57ba35ae317ec6370836bda0012db8",
|
"content-hash": "9ffd10f98137e8975de2c04ac2412ed5",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "1tomany/rich-bundle",
|
"name": "1tomany/rich-bundle",
|
||||||
@@ -4757,6 +4757,65 @@
|
|||||||
],
|
],
|
||||||
"time": "2023-12-12T12:06:11+00:00"
|
"time": "2023-12-12T12:06:11+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/icalendar-generator",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/icalendar-generator.git",
|
||||||
|
"reference": "32797f6e5afa3142d073f38d5f22ab377f4d8f90"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/icalendar-generator/zipball/32797f6e5afa3142d073f38d5f22ab377f4d8f90",
|
||||||
|
"reference": "32797f6e5afa3142d073f38d5f22ab377f4d8f90",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"larapack/dd": "^1.1",
|
||||||
|
"nesbot/carbon": "^3.5",
|
||||||
|
"pestphp/pest": "^2.34",
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"spatie/pest-plugin-snapshots": "^2.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\IcalendarGenerator\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Ruben Van Assche",
|
||||||
|
"email": "ruben@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Build calendars in the iCalendar format",
|
||||||
|
"homepage": "https://github.com/spatie/icalendar-generator",
|
||||||
|
"keywords": [
|
||||||
|
"calendar",
|
||||||
|
"iCalendar",
|
||||||
|
"ical",
|
||||||
|
"ics",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/icalendar-generator/issues",
|
||||||
|
"source": "https://github.com/spatie/icalendar-generator/tree/3.0.0"
|
||||||
|
},
|
||||||
|
"time": "2025-04-17T14:50:03+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spomky-labs/pki-framework",
|
"name": "spomky-labs/pki-framework",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ security:
|
|||||||
# Easy way to control access for large sections of your site
|
# Easy way to control access for large sections of your site
|
||||||
# Note: Only the *first* access control that matches will be used
|
# Note: Only the *first* access control that matches will be used
|
||||||
access_control:
|
access_control:
|
||||||
|
- { path: ^/monitors/ical/, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/reset-password, roles: PUBLIC_ACCESS }
|
- { path: ^/reset-password, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/getting-started, roles: PUBLIC_ACCESS }
|
- { path: ^/getting-started, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/register, roles: PUBLIC_ACCESS }
|
- { path: ^/register, roles: PUBLIC_ACCESS }
|
||||||
|
|||||||
@@ -1,20 +1,6 @@
|
|||||||
FROM dunglas/frankenphp:php8.4-alpine
|
ARG APP_VERSION
|
||||||
|
|
||||||
ENV SERVER_NAME=":80"
|
FROM code.caldwell.digital/torsearch/torsearch-app:${APP_VERSION}
|
||||||
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
|
|
||||||
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
|
|
||||||
|
|
||||||
ARG APP_VERSION="0.dev"
|
|
||||||
ENV APP_VERSION="${APP_VERSION}"
|
|
||||||
|
|
||||||
RUN install-php-extensions \
|
|
||||||
pdo_mysql \
|
|
||||||
gd \
|
|
||||||
intl \
|
|
||||||
zip \
|
|
||||||
opcache
|
|
||||||
|
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
|
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,13 @@ class SeedDatabaseCommand extends Command
|
|||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'type' => 'download'
|
'type' => 'download'
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'id' => 'enable_ical_up_ep',
|
||||||
|
'name' => 'Enable a publicly available iCal calendar?',
|
||||||
|
'description' => 'Enable a publicly accessible iCal URL for your upcoming episodes.',
|
||||||
|
'enabled' => false,
|
||||||
|
'type' => 'calendar'
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Monitor\Dto;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
|
|
||||||
class UpcomingEpisode
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public string $title,
|
|
||||||
public string $airDate {
|
|
||||||
get => Carbon::parse($this->airDate)->format('m/d/Y');
|
|
||||||
},
|
|
||||||
public string $episodeTitle,
|
|
||||||
public int $episodeNumber,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
41
src/Monitor/Framework/Controller/CalendarController.php
Normal file
41
src/Monitor/Framework/Controller/CalendarController.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Monitor\Framework\Controller;
|
||||||
|
|
||||||
|
use Aimeos\Map;
|
||||||
|
use App\Monitor\Framework\Repository\MonitorRepository;
|
||||||
|
use App\User\Framework\Entity\User;
|
||||||
|
use Spatie\IcalendarGenerator\Components\Calendar;
|
||||||
|
use Spatie\IcalendarGenerator\Components\Event;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
class CalendarController extends AbstractController
|
||||||
|
{
|
||||||
|
#[IsGranted('PUBLIC_ACCESS')]
|
||||||
|
#[Route('/monitors/ical/{email:user}/upcoming-episodes.ics', name: 'app.monitors.ical')]
|
||||||
|
public function icalAction(MonitorRepository $monitorRepository, User $user)
|
||||||
|
{
|
||||||
|
if (false === $user->hasICalEnabled()) {
|
||||||
|
return new Response('Calendar not found.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$calendar = Calendar::create()
|
||||||
|
->name('Upcoming Episodes')
|
||||||
|
->refreshInterval(10);
|
||||||
|
|
||||||
|
$monitors = $monitorRepository->whereAirDateNotNull();
|
||||||
|
$calendar->event(Map::from($monitors)->map(function ($monitor) {
|
||||||
|
return new Event($monitor->getTitle())
|
||||||
|
->startsAt($monitor->getAirDate())
|
||||||
|
->fullDay();
|
||||||
|
})->toArray());
|
||||||
|
|
||||||
|
return new Response($calendar->get(), 200, [
|
||||||
|
'Content-Type' => 'text/calendar',
|
||||||
|
'Content-Disposition' => 'inline; filename="upcoming-episodes.ics"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,8 @@ class GetMediaInfoHandler implements HandlerInterface
|
|||||||
public function handle(CommandInterface $command): ResultInterface
|
public function handle(CommandInterface $command): ResultInterface
|
||||||
{
|
{
|
||||||
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
$media = $this->tmdb->mediaDetails($command->imdbId, $command->mediaType);
|
||||||
|
$relatedMedia = $this->tmdb->relatedMedia($media->tmdbId, $command->mediaType);
|
||||||
|
|
||||||
return new GetMediaInfoResult($media, $command->season, $command->episode);
|
return new GetMediaInfoResult($media, $relatedMedia, $command->season, $command->episode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class GetMediaInfoResult implements ResultInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public TmdbResult $media,
|
public TmdbResult $media,
|
||||||
|
public array $relatedMedia,
|
||||||
public ?int $season,
|
public ?int $season,
|
||||||
public ?int $episode,
|
public ?int $episode,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ use App\Search\Action\Handler\SearchHandler;
|
|||||||
use App\Search\Action\Input\GetMediaInfoInput;
|
use App\Search\Action\Input\GetMediaInfoInput;
|
||||||
use App\Search\Action\Input\SearchInput;
|
use App\Search\Action\Input\SearchInput;
|
||||||
use App\Search\Action\Result\RedirectToMediaResult;
|
use App\Search\Action\Result\RedirectToMediaResult;
|
||||||
use App\Tmdb\TmdbResult;
|
|
||||||
use App\Torrentio\Action\Command\GetMovieOptionsCommand;
|
|
||||||
use App\Torrentio\Action\Command\GetTvShowOptionsCommand;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
|||||||
@@ -247,6 +247,21 @@ class Tmdb
|
|||||||
return $series;
|
return $series;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function relatedMedia(string $tmdbId, string $mediaType, int $maxResults = 6)
|
||||||
|
{
|
||||||
|
$repos = [
|
||||||
|
'movies' => $this->movieRepository,
|
||||||
|
'tvshows' => $this->tvRepository,
|
||||||
|
];
|
||||||
|
|
||||||
|
$results = $repos[$mediaType]->getRecommendations($tmdbId);
|
||||||
|
return Map::from(array_values($results->toArray()))
|
||||||
|
->slice(0, 6)
|
||||||
|
->map(function ($result) use ($mediaType) {
|
||||||
|
return $this->parseResult($result, $mediaType);
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
public function mediaDetails(string $id, string $type)
|
public function mediaDetails(string $id, string $type)
|
||||||
{
|
{
|
||||||
$id = $this->find($id);
|
$id = $this->find($id);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace App\Twig\Components;
|
namespace App\Twig\Components;
|
||||||
|
|
||||||
use Aimeos\Map;
|
use Aimeos\Map;
|
||||||
use App\Monitor\Dto\UpcomingEpisode;
|
use App\Monitor\Factory\UpcomingEpisodeDto;
|
||||||
use App\Monitor\Framework\Entity\Monitor;
|
use App\Monitor\Framework\Entity\Monitor;
|
||||||
use App\Tmdb\Tmdb;
|
use App\Tmdb\Tmdb;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
@@ -70,7 +70,7 @@ final class UpcomingEpisodes extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $episodes->map(function (array $episode) use ($monitor) {
|
return $episodes->map(function (array $episode) use ($monitor) {
|
||||||
return new UpcomingEpisode(
|
return new UpcomingEpisodeDto(
|
||||||
$monitor->getTitle(),
|
$monitor->getTitle(),
|
||||||
$episode['air_date'],
|
$episode['air_date'],
|
||||||
$episode['name'],
|
$episode['name'],
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Action\Command;
|
||||||
|
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface;
|
||||||
|
|
||||||
|
/** @implements CommandInterface<SaveUserMediaPreferencesCommand> */
|
||||||
|
class SaveUserCalendarPreferencesCommand implements CommandInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $enable_ical_up_ep,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Action\Handler;
|
||||||
|
|
||||||
|
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
||||||
|
use App\User\Action\Result\SaveUserDownloadPreferencesResult;
|
||||||
|
use App\User\Action\Result\SaveUserMediaPreferencesResult;
|
||||||
|
use App\User\Framework\Entity\User;
|
||||||
|
use App\User\Framework\Entity\UserPreference;
|
||||||
|
use App\User\Framework\Repository\PreferencesRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||||
|
use OneToMany\RichBundle\Contract\HandlerInterface;
|
||||||
|
use OneToMany\RichBundle\Contract\ResultInterface as R;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
|
||||||
|
/** @implements HandlerInterface<SaveUserMediaPreferencesCommand> */
|
||||||
|
class SaveUserCalendarPreferencesHandler implements HandlerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly PreferencesRepository $preferenceRepository,
|
||||||
|
private readonly Security $token,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(C $command): R
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $this->token->getUser();
|
||||||
|
|
||||||
|
foreach ($command as $preference => $value) {
|
||||||
|
if ($user->hasUserPreference($preference)) {
|
||||||
|
$user->updateUserPreference($preference, $value);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$preference = $this->preferenceRepository->find($preference);
|
||||||
|
|
||||||
|
$user->addUserPreference(
|
||||||
|
(new UserPreference())
|
||||||
|
->setUser($user)
|
||||||
|
->setPreference($preference)
|
||||||
|
->setPreferenceValue($value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return new SaveUserDownloadPreferencesResult($user->getDownloadPreferences());
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/User/Action/Input/SaveUserCalendarPreferencesInput.php
Normal file
29
src/User/Action/Input/SaveUserCalendarPreferencesInput.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Action\Input;
|
||||||
|
|
||||||
|
use App\User\Action\Command\SaveUserCalendarPreferencesCommand;
|
||||||
|
use App\User\Action\Command\SaveUserDownloadPreferencesCommand;
|
||||||
|
use OneToMany\RichBundle\Attribute\SourceRequest;
|
||||||
|
use OneToMany\RichBundle\Attribute\SourceSecurity;
|
||||||
|
use OneToMany\RichBundle\Contract\CommandInterface as C;
|
||||||
|
use OneToMany\RichBundle\Contract\InputInterface;
|
||||||
|
|
||||||
|
/** @implements InputInterface<SaveUserDownloadPreferencesInput, SaveUserDownloadPreferencesCommand> */
|
||||||
|
class SaveUserCalendarPreferencesInput implements InputInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[SourceSecurity]
|
||||||
|
public mixed $userId,
|
||||||
|
|
||||||
|
#[SourceRequest('enable_ical_up_ep', nullify: true)]
|
||||||
|
public bool $enableIcalUpcomingEpisodes,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function toCommand(): C
|
||||||
|
{
|
||||||
|
return new SaveUserCalendarPreferencesCommand(
|
||||||
|
$this->enableIcalUpcomingEpisodes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ namespace App\User\Framework\Controller\Web;
|
|||||||
|
|
||||||
use App\Base\Service\Broadcaster;
|
use App\Base\Service\Broadcaster;
|
||||||
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
use App\User\Action\Command\SaveUserMediaPreferencesCommand;
|
||||||
|
use App\User\Action\Handler\SaveUserCalendarPreferencesHandler;
|
||||||
use App\User\Action\Handler\SaveUserDownloadPreferencesHandler;
|
use App\User\Action\Handler\SaveUserDownloadPreferencesHandler;
|
||||||
use App\User\Action\Handler\SaveUserMediaPreferencesHandler;
|
use App\User\Action\Handler\SaveUserMediaPreferencesHandler;
|
||||||
|
use App\User\Action\Input\SaveUserCalendarPreferencesInput;
|
||||||
use App\User\Action\Input\SaveUserDownloadPreferencesInput;
|
use App\User\Action\Input\SaveUserDownloadPreferencesInput;
|
||||||
use App\User\Action\Input\SaveUserMediaPreferencesInput;
|
use App\User\Action\Input\SaveUserMediaPreferencesInput;
|
||||||
use App\User\Database\CountryLanguages;
|
use App\User\Database\CountryLanguages;
|
||||||
@@ -33,15 +35,15 @@ class PreferencesController extends AbstractController
|
|||||||
public function mediaPreferences(): Response
|
public function mediaPreferences(): Response
|
||||||
{
|
{
|
||||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||||
|
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||||
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
||||||
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
||||||
|
|
||||||
// dd($form);
|
|
||||||
|
|
||||||
return $this->render(
|
return $this->render(
|
||||||
'user/preferences.html.twig',
|
'user/preferences.html.twig',
|
||||||
[
|
[
|
||||||
'downloadPreferences' => $downloadPreferences,
|
'downloadPreferences' => $downloadPreferences,
|
||||||
|
'calendarPreferences' => $calendarPreferences,
|
||||||
'preferences_form' => $form,
|
'preferences_form' => $form,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -54,8 +56,8 @@ class PreferencesController extends AbstractController
|
|||||||
): Response
|
): Response
|
||||||
{
|
{
|
||||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||||
|
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||||
$form = $this->createForm(UserMediaPreferencesForm::class);
|
$form = $this->createForm(UserMediaPreferencesForm::class);
|
||||||
|
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
@@ -69,6 +71,7 @@ class PreferencesController extends AbstractController
|
|||||||
'user/preferences.html.twig',
|
'user/preferences.html.twig',
|
||||||
[
|
[
|
||||||
'downloadPreferences' => $downloadPreferences,
|
'downloadPreferences' => $downloadPreferences,
|
||||||
|
'calendarPreferences' => $calendarPreferences,
|
||||||
'preferences_form' => $form,
|
'preferences_form' => $form,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -81,6 +84,7 @@ class PreferencesController extends AbstractController
|
|||||||
): Response
|
): Response
|
||||||
{
|
{
|
||||||
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
$downloadPreferences = $this->getUser()->getDownloadPreferences();
|
||||||
|
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||||
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
||||||
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
||||||
|
|
||||||
@@ -95,6 +99,34 @@ class PreferencesController extends AbstractController
|
|||||||
'user/preferences.html.twig',
|
'user/preferences.html.twig',
|
||||||
[
|
[
|
||||||
'downloadPreferences' => $downloadPreferences,
|
'downloadPreferences' => $downloadPreferences,
|
||||||
|
'calendarPreferences' => $calendarPreferences,
|
||||||
|
'preferences_form' => $form,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/user/preferences/calendar', 'app.save.calendar-preferences', methods: ['POST'])]
|
||||||
|
public function saveCalendarPreferences(
|
||||||
|
SaveUserCalendarPreferencesInput $input,
|
||||||
|
SaveUserCalendarPreferencesHandler $handler,
|
||||||
|
): Response
|
||||||
|
{
|
||||||
|
$calendarPreferences = $this->getUser()->getCalendarPreferences();
|
||||||
|
$formData = (array) UserPreferencesFactory::createFromUser($this->getUser());
|
||||||
|
$form = $this->createForm(UserMediaPreferencesForm::class, $formData);
|
||||||
|
|
||||||
|
$handler->handle($input->toCommand());
|
||||||
|
|
||||||
|
$this->broadcaster->alert(
|
||||||
|
title: 'Success',
|
||||||
|
message: 'Your calendar preferences have been saved.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->render(
|
||||||
|
'user/preferences.html.twig',
|
||||||
|
[
|
||||||
|
'downloadPreferences' => $this->getUser()->getDownloadPreferences(),
|
||||||
|
'calendarPreferences' => $calendarPreferences,
|
||||||
'preferences_form' => $form,
|
'preferences_form' => $form,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -327,4 +327,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCalendarPreferences(): array
|
||||||
|
{
|
||||||
|
return Map::from($this->userPreferences)
|
||||||
|
->rekey(fn(UserPreference $userPreference) => $userPreference->getPreference()->getId())
|
||||||
|
->filter(fn(UserPreference $userPreference) => $userPreference->getPreference()->getType() === 'calendar')
|
||||||
|
->toArray()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasICalEnabled(): bool
|
||||||
|
{
|
||||||
|
return $this->hasUserPreference('enable_ical_up_ep') &&
|
||||||
|
(bool) $this->getUserPreference('enable_ical_up_ep')->getPreferenceValue() === true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<twig:SearchBar />
|
<twig:SearchBar />
|
||||||
<div class="md:flex md:items-center md:gap-12">
|
<div class="md:flex md:items-center md:gap-12">
|
||||||
<nav aria-label="Global" class="md:block">
|
<nav aria-label="Global" class="md:block">
|
||||||
<ul class="flex items-center gap-6 text-sm">
|
<ul class="ml-4 flex items-end md:items-center md:gap-6 text-sm">
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false" title="View upcoming episodes of the shows you're subscribed to.">
|
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false" title="View upcoming episodes of the shows you're subscribed to.">
|
||||||
<twig:ux:icon name="solar:calendar-linear" width="25px" class="text-orange-500" />
|
<twig:ux:icon name="solar:calendar-linear" width="25px" class="text-orange-500" />
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if this.isWidget and this.monitors.items|length > 5 %}
|
{% if this.isWidget and this.monitors.items|length > 5 %}
|
||||||
<tr id="monitor_view_all">
|
<tr id="monitor_view_all">
|
||||||
<td colspan="100%" class="py-2 whitespace-nowrap bg-gray-400 dark:bg-gray-700 uppercase text-xs font-medium text-center text-black dark:text-white min-w-[50ch] max-w-[50ch] truncate">
|
<td colspan="100%" class="py-2 whitespace-nowrap bg-orange-500/80 uppercase text-xs font-medium text-center truncate dark:text-black">
|
||||||
<a href="{{ path('app_monitors') }}">View All Monitors</a>
|
<a href="{{ path('app_monitors') }}">View All Monitors</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block h2 %}Upcoming Episodes{% endblock %}
|
{% block h2 %}Upcoming Episodes{% endblock %}
|
||||||
|
|
||||||
{% block action_buttons %}
|
{% block action_buttons %}
|
||||||
<a href="{{ path('app.monitor.upcoming-episodes') }}"
|
<a href="{{ path('app.monitor.upcoming-episodes') }}" data-turbo="false"
|
||||||
class="h-6 bg-orange-500/80 hover:bg-orange-600/80 px-2 text-white rounded-ms text-sm font-semibold">
|
class="h-6 bg-orange-500/80 hover:bg-orange-600/80 px-2 text-white rounded-ms text-sm font-semibold">
|
||||||
Upcoming Episodes
|
Upcoming Episodes
|
||||||
</a>
|
</a>
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<twig:Card title="Upcoming episodes of shows your monitoring">
|
<twig:Card title="Upcoming episodes of shows your monitoring">
|
||||||
|
<a href="{{ path('app.monitors.ical', {email: app.user.email}) }}" title="Subscribe to the 'Upcoming Episodes' calendar via iCal. Click to export the events to a .ics file or copy the link and use it to subscribe in a calendar app that supports iCal/ics calendars." class="mb-2 self-end dark:text-white decoration-underline">
|
||||||
|
<twig:ux:icon name="lets-icons:calendar-add-light" width="24" class="text-orange-500" />
|
||||||
|
</a>
|
||||||
<div id="calendar" class="text-white">
|
<div id="calendar" class="text-white">
|
||||||
</div>
|
</div>
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
{% block title %}{{ results.media.title }} — Download Options — Torsearch{% endblock %}
|
{% block title %}{{ results.media.title }} — Download Options — Torsearch{% endblock %}
|
||||||
|
|
||||||
|
{% block h2 %}Media Results{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="p-4 flex flex-col grow gap-4">
|
<div class="p-4 flex flex-col grow gap-4">
|
||||||
<h2 class="mb-2 text-3xl font-bold text-gray-50">Media Results</h2>
|
|
||||||
<div class="flex flex-row w-full gap-2">
|
<div class="flex flex-row w-full gap-2">
|
||||||
<twig:Card title="" class="w-full" contentClass="flex flex-col gap-4 justify-between w-full text-gray-50">
|
<twig:Card title="" class="w-full" contentClass="flex flex-col gap-4 justify-between w-full text-gray-50">
|
||||||
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
<div class="p-2 md:p-4 flex flex-col md:flex-row gap-6">
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<div class="mb-4 flex flex-row gap-2 justify-between">
|
<div class="mb-4 flex flex-row gap-2 justify-between">
|
||||||
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
|
<h3 class="text-xl font-medium leading-tight font-bold text-gray-50">
|
||||||
{{ results.media.title }} ({{ results.media.year }})
|
{{ results.media.title }} ({{ results.media.year|date('Y') }})
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{% if results.media.mediaType == "tvshows" %}
|
{% if results.media.mediaType == "tvshows" %}
|
||||||
@@ -95,12 +96,35 @@
|
|||||||
{% elseif "tvshows" == results.media.mediaType %}
|
{% elseif "tvshows" == results.media.mediaType %}
|
||||||
<twig:TvEpisodeList
|
<twig:TvEpisodeList
|
||||||
results="results"
|
results="results"
|
||||||
:imdbId="results.media.imdbId" :season="results.season" :perPage="20" :pageNumber="1"
|
loading="defer"
|
||||||
:tmdbId="results.media.tmdbId" :title="results.media.title" loading="defer" :episodeNumber="results.episode"
|
:imdbId="results.media.imdbId"
|
||||||
|
:season="results.season"
|
||||||
|
:perPage="20"
|
||||||
|
:pageNumber="1"
|
||||||
|
:tmdbId="results.media.tmdbId"
|
||||||
|
:title="results.media.title"
|
||||||
|
:episodeNumber="results.episode"
|
||||||
/>
|
/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<twig:Card title="Related Media" contentClass="flex flex-col gap-4 text-white">
|
||||||
|
<p>Results similar to "{{ results.media.title }}" that you may be interested in.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4 md:flex flex-col md:flex-row justify-between w-full">
|
||||||
|
{% for media in results.relatedMedia %}
|
||||||
|
<twig:Poster imdbId="{{ media.imdbId }}"
|
||||||
|
tmdbId="{{ media.tmdbId }}"
|
||||||
|
title="{{ media.title }}"
|
||||||
|
description="{{ media.description }}"
|
||||||
|
image="{{ media.poster }}"
|
||||||
|
year="{{ media.year }}"
|
||||||
|
mediaType="{{ media.mediaType }}"
|
||||||
|
/>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</twig:Card>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -36,4 +36,25 @@
|
|||||||
</form>
|
</form>
|
||||||
</twig:Card>
|
</twig:Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 flex flex-col md:flex-row gap-2">
|
||||||
|
<twig:Card title="Calendar Preferences" class="w-full">
|
||||||
|
<p class="text-gray-50 mb-4">Manage your Upcoming Episodes calendar.</p>
|
||||||
|
<form id="calendar_preferences" class="flex flex-col" name="calendar_preferences" method="post" action="{{ path('app.save.calendar-preferences') }}">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-row gap-2 mb-1">
|
||||||
|
<input type="hidden" name="enable_ical_up_ep" id="enable_ical_up_ep_hidden" value="0" />
|
||||||
|
<input type="checkbox" name="enable_ical_up_ep" id="enable_ical_up_ep" value="1" {{ calendarPreferences['enable_ical_up_ep'].getPreferenceValue() == true ? 'checked' }} />
|
||||||
|
<label class="text-gray-50" for="enable_ical_up_ep">Enable a publicly available iCal calendar?</label>
|
||||||
|
</div>
|
||||||
|
<small class="text-gray-50 mb-4">Enabling the iCal calendar will allow you to subscribe from iCal
|
||||||
|
supporting clients. This endpoint will be publicly available with no authentication required.
|
||||||
|
Disabling this option will disable the calendar and public endpoint for your user.
|
||||||
|
This will not affect the calendar within the app.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button class="px-1.5 py-1 max-w-20 rounded-md bg-green-600 text-white" type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</twig:Card>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user