Compare commits

...

2 Commits

Author SHA1 Message Date
9db0bfd4c6 fix: filter style tweaks, loading icon patch 2025-07-26 17:39:28 -05:00
18a165fc40 wip: filter uses form object 2025-07-26 15:46:43 -05:00
11 changed files with 144 additions and 157 deletions

View File

@@ -14,7 +14,22 @@ export default class extends Controller {
static targets = ['icon'] static targets = ['icon']
connect() { connect() {
this.element.hideIcon = this.hideIcon.bind(this);
this.element.showIcon = this.showIcon.bind(this);
this.element.toggleIcon = this.toggleIcon.bind(this);
this.element.isVisibile = this.isVisible.bind(this);
}
isVisible() {
return !this.iconTarget.classList.contains('hidden');
}
showIcon() {
this.iconTarget.classList.remove('hidden');
}
hideIcon() {
this.iconTarget.classList.add('hidden');
} }
toggleIcon() { toggleIcon() {

View File

@@ -22,7 +22,7 @@ export default class extends Controller {
defaultOptions = '<option value="">n/a</option><option value="-">-</option>'; defaultOptions = '<option value="">n/a</option><option value="-">-</option>';
static outlets = ['tv-episode-list'] static outlets = ['tv-episode-list']
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected'] static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'loadingIcon', 'selectAll', 'downloadSelected']
static values = { static values = {
'imdbId': String, 'imdbId': String,
'media-type': String, 'media-type': String,
@@ -34,19 +34,24 @@ export default class extends Controller {
if (this.mediaTypeValue === "tvshows") { if (this.mediaTypeValue === "tvshows") {
this.activeFilter['season'] = 1; this.activeFilter['season'] = 1;
} }
await this.filter(); this.filter();
this.setTimerToStopLoadingIcon();
document.addEventListener('optionsLoaded', this.loadOptions.bind(this)); document.addEventListener('optionsLoaded', this.loadOptions.bind(this));
} }
setTimerToStopLoadingIcon() {
setTimeout(() => this.loadingIconTarget.hideIcon(), 10000);
}
// Event is fired from movies/tvshows controllers to populate this data // Event is fired from movies/tvshows controllers to populate this data
async loadOptions({detail: { options }}) { loadOptions({detail: { options }}) {
await options.forEach((option) => { options.forEach((option) => {
this.addLanguages(option); this.addLanguages(option);
this.addProviders(option); this.addProviders(option);
this.addQualities(option); this.addQualities(option);
}) })
await this.filter(); this.filter();
} }
selectAllEpisodes() { selectAllEpisodes() {
@@ -67,7 +72,7 @@ export default class extends Controller {
this.languages.push(language); this.languages.push(language);
} }
}); });
const preferred = JSON.parse(this.languageTarget.dataset.preferred); const preferred = JSON.parse(this.languageTarget.dataset.preferred) ?? [];
this.languageTarget.innerHTML = this.#serializeSelectOptions(this.languages); this.languageTarget.innerHTML = this.#serializeSelectOptions(this.languages);
this.languageTarget.tomselect.items = preferred; this.languageTarget.tomselect.items = preferred;
} }
@@ -76,7 +81,7 @@ export default class extends Controller {
if (!this.providers.includes(option.provider)) { if (!this.providers.includes(option.provider)) {
this.providers.push(option.provider); this.providers.push(option.provider);
} }
const preferred = JSON.parse(this.providerTarget.dataset.preferred); const preferred = JSON.parse(this.providerTarget.dataset.preferred) ?? [];
this.providerTarget.innerHTML = this.#serializeSelectOptions(this.providers); this.providerTarget.innerHTML = this.#serializeSelectOptions(this.providers);
this.providerTarget.tomselect.items = preferred; this.providerTarget.tomselect.items = preferred;
@@ -96,7 +101,7 @@ export default class extends Controller {
this.qualityTarget.tomselect.items = preferred; this.qualityTarget.tomselect.items = preferred;
} }
async filter() { filter() {
const downloadSeasonSpan = document.querySelector("#downloadSeasonModal"); const downloadSeasonSpan = document.querySelector("#downloadSeasonModal");
this.activeFilter = { this.activeFilter = {

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="currentColor" d="m0 11l2-2l5 5L18 3l2 2L7 18z"/></svg>

After

Width:  |  Height:  |  Size: 127 B

View File

@@ -9,16 +9,21 @@ use App\User\Database\ResolutionList;
use App\User\Dto\PreferenceOptions; use App\User\Dto\PreferenceOptions;
use App\User\Dto\PreferenceOptionsFactory; use App\User\Dto\PreferenceOptionsFactory;
use App\User\Dto\UserPreferencesFactory; use App\User\Dto\UserPreferencesFactory;
use App\User\Framework\Form\UserMediaPreferencesForm;
use App\User\Framework\Repository\PreferencesRepository; use App\User\Framework\Repository\PreferencesRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Form\FormInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait; use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsLiveComponent] #[AsLiveComponent]
final class Filter extends AbstractController final class Filter extends AbstractController
{ {
use DefaultActionTrait; use DefaultActionTrait;
use ComponentWithFormTrait;
public array $preferences = []; public array $preferences = [];
@@ -43,4 +48,9 @@ final class Filter extends AbstractController
{ {
return CodecList::asSelectOptions(); return CodecList::asSelectOptions();
} }
protected function instantiateForm(): FormInterface
{
return $this->createForm(UserMediaPreferencesForm::class, UserPreferencesFactory::createFromUser($this->getUser()));
}
} }

View File

@@ -8,7 +8,10 @@ use App\User\Database\CountryLanguages;
use App\User\Database\ProviderList; use App\User\Database\ProviderList;
use App\User\Database\QualityList; use App\User\Database\QualityList;
use App\User\Database\ResolutionList; use App\User\Database\ResolutionList;
use App\User\Dto\UserPreferences;
use App\User\Dto\UserPreferencesFactory;
use App\User\Framework\Repository\PreferenceOptionRepository; use App\User\Framework\Repository\PreferenceOptionRepository;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Event\PreSetDataEvent; use Symfony\Component\Form\Event\PreSetDataEvent;
use Symfony\Component\Form\Event\PreSubmitEvent; use Symfony\Component\Form\Event\PreSubmitEvent;
@@ -21,9 +24,14 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class UserMediaPreferencesForm extends AbstractType class UserMediaPreferencesForm extends AbstractType
{ {
private UserPreferences $userPreferences;
public function __construct( public function __construct(
private readonly UrlGeneratorInterface $urlGenerator, private readonly UrlGeneratorInterface $urlGenerator,
) {} private readonly Security $security,
) {
$this->userPreferences = UserPreferencesFactory::createFromUser($security->getUser());
}
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
@@ -37,8 +45,17 @@ class UserMediaPreferencesForm extends AbstractType
private function addChoiceField(FormBuilderInterface $builder, string $fieldName, array $choices): void private function addChoiceField(FormBuilderInterface $builder, string $fieldName, array $choices): void
{ {
$question = [ $question = [
'attr' => ['class' => 'w-64 text-input mb-4'], 'attr' => [
'label_attr' => ['class' => 'w-64 text-white block font-semibold mb-2'], 'class' => 'min-w-24 text-input mb-4',
'data-result-filter-target' => $fieldName,
'data-controller' => 'symfony--ux-autocomplete--autocomplete',
'data-symfony--ux-autocomplete--autocomplete-tom-select-options-value' => '{"highlight":false}',
'data-preferred' => \json_encode($this->userPreferences->$fieldName),
],
'row_attr' => [
'class' => 'filter-label'
],
'label_attr' => ['class' => 'text-white block font-semibold mb-2'],
'choices' => $this->addDefaultChoice($choices), 'choices' => $this->addDefaultChoice($choices),
'required' => false, 'required' => false,
'multiple' => true, 'multiple' => true,
@@ -50,6 +67,9 @@ class UserMediaPreferencesForm extends AbstractType
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'action' => $this->urlGenerator->generate('app_user_media_preferences_submit'), 'action' => $this->urlGenerator->generate('app_user_media_preferences_submit'),
'attr' => [
'class' => 'filter-items w-full p-4 bg-black/20 border-2 border-orange-500 text-md text-gray-500 dark:text-gray-50 rounded-lg',
]
]); ]);
} }

View File

@@ -20,6 +20,7 @@ module.exports = {
"bg-orange-400", "bg-orange-400",
"bg-blue-600", "bg-blue-600",
"bg-rose-600", "bg-rose-600",
"bg-black/20",
"alert-success", "alert-success",
"alert-warning", "alert-warning",
"font-bold", "font-bold",

View File

@@ -6,108 +6,21 @@
data-result-filter-tv-episode-list-outlet=".episode-list" data-result-filter-tv-episode-list-outlet=".episode-list"
data-action="change->result-filter#filter action-button:downloadSeason@window->result-filter#downloadSeason" data-action="change->result-filter#filter action-button:downloadSeason@window->result-filter#downloadSeason"
> >
<div class="filter-items w-full p-4 flex flex-col md:flex-row gap-4 bg-black/20 border-2 border-orange-500 text-md text-gray-500 dark:text-gray-50 rounded-lg"> {% set preferences_form = form %}
<label for="resolution" class="filter-label"> {{ form_start(preferences_form) }}
Resolution <div class="flex flex-col md:flex-row gap-2 justify-between">
<select id="resolution" {{ form_row(preferences_form.resolution) }}
multiple="multiple" {{ form_row(preferences_form.codec) }}
data-result-filter-target="resolution" {{ form_row(preferences_form.language) }}
class="px-1 py-0.5 bg-stone-100 placeholder-text-gray-50 text-gray-50" {{ form_row(preferences_form.provider) }}
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', { {{ form_row(preferences_form.quality) }}
create: false,
tomSelectOptions: {
highlight: false,
}
}) }}
>
<option value="">n/a</option>
{% for name, value in this.resolutionOptions %}
<option value="{{ value }}"
{{ value in this.userPreferences['resolution'] ? 'selected' }}
>{{ name }}</option>
{% endfor %}
</select>
</label>
<label for="codec" class="filter-label">
Codec
<select id="codec"
multiple="multiple"
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
create: false,
tomSelectOptions: {
highlight: false,
}
}) }}
data-result-filter-target="codec" class="px-1 py-0.5 bg-stone-100 text-gray-50">
<option value="">n/a</option>
{% for name, value in this.codecOptions %}
<option value="{{ value }}"
{{ value in this.userPreferences['codec'] ? 'selected' }}
>{{ name }}</option>
{% endfor %}
</select>
</label>
<label for="language" class="filter-label">
Language
<select id="language"
multiple="multiple"
data-result-filter-target="language"
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
create: false,
tomSelectOptions: {
highlight: false,
}
}) }}
class="px-1 py-0.5 bg-stone-100 text-gray-50"
{% if this.userPreferences['language'] != null %}
data-preferred="{{ this.userPreferences['language']|json_encode }}"
{% endif %}
>
</select>
</label>
<label for="provider" class="filter-label">
Provider
<select id="provider"
multiple="multiple"
data-result-filter-target="provider"
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
create: false,
tomSelectOptions: {
highlight: false,
}
}) }}
class="px-1 py-0.5 bg-stone-100 text-gray-50"
{% if this.userPreferences['provider'] != null %}
data-preferred="{{ this.userPreferences['provider']|json_encode }}"
{% endif %}
>
</select>
</label>
<label for="quality" class="filter-label">
Quality
<select id="quality"
multiple="multiple"
data-result-filter-target="quality"
class="px-1 py-0.5 bg-stone-100 text-gray-50"
data-preferred="{{ this.userPreferences['quality']|json_encode }}"
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
create: false,
tomSelectOptions: {
highlight: false,
}
}) }}
>
</select>
</label>
{% if results.media.mediaType == "tvshows" %} {% if results.media.mediaType == "tvshows" %}
<label for="season" class="filter-label"> <div class="flex flex-col gap-1 md:gap-3">
<label for="season">
Season Season
<select id="season" name="season" value="1" data-result-filter-target="season" class="px-1 py-0.5 bg-stone-100 text-gray-800" </label>
<select id="season" name="season" value="1" data-result-filter-target="season" class="px-1 mb-4 py-1 md:py-2 text-center bg-orange-500 text-white rounded-ms"
{{ stimulus_action('result_filter', 'setSeason', 'change') }} {{ stimulus_action('result_filter', 'setSeason', 'change') }}
{{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }} {{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }}
> >
@@ -119,13 +32,18 @@
>{{ season }}</option> >{{ season }}</option>
{% endfor %} {% endfor %}
</select> </select>
</label> </div>
{% endif %} {% endif %}
<span {{ stimulus_controller('loading_icon', {total: (results.media.mediaType == "tvshows") ? results.media.episodes[1]|length : 1, count: 0}) }} </div>
{{ form_end(preferences_form) }}
<div class="flex flex-col md:flex-row justify-between">
<span
{{ stimulus_target('result-filter', 'loadingIcon') }}
{{ stimulus_controller('loading_icon', {total: (results.media.mediaType == "tvshows") ? results.media.episodes[1]|length : 1, count: 0}) }}
class="loading-icon"> class="loading-icon">
<twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" class="text-end" /> <twig:ux:icon name="codex:loader" height="20" width="20" data-loading-icon-target="icon" class="text-end" />
</span> </span>
</div>
{% if results.media.mediaType == "tvshows" %} {% if results.media.mediaType == "tvshows" %}
<div class="flex flex-row gap-2 justify-end px-8"> <div class="flex flex-row gap-2 justify-end px-8">
@@ -148,4 +66,5 @@
/> />
</div> </div>
{% endif %} {% endif %}
</div>
</div> </div>

View File

@@ -10,7 +10,7 @@
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<h3 class="mb-4 text-xl font-medium leading-tight font-bold text-gray-50"> <h3 class="mb-4 text-xl font-medium leading-tight font-bold text-gray-50">
{{ title }} - {{ year }} {{ title }} ({{ year }})
</h3> </h3>
<p class="hidden md:block md:text-gray-50"> <p class="hidden md:block md:text-gray-50">
{{ description }} {{ description }}

View File

@@ -0,0 +1,8 @@
<button class="submit-button flex flex-row gap-2 items-center">
{% if show_icon|default %}
<twig:ux:icon name="zondicons:checkmark" width=".8rem" class="text-green-500" />
{% endif %}
{% if false == icon_only|default(false) %}
{{ text|default('submit') }}
{% endif %}
</button>

View File

@@ -19,7 +19,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 }})
</h3> </h3>
{% if results.media.mediaType == "tvshows" %} {% if results.media.mediaType == "tvshows" %}

View File

@@ -6,16 +6,24 @@
<div class="p-4 flex flex-col md:flex-row gap-2"> <div class="p-4 flex flex-col md:flex-row gap-2">
<twig:Card title="Media Preferences" class="w-full"> <twig:Card title="Media Preferences" class="w-full">
<p class="text-gray-50 mb-4">Define a filter to be pre-applied to your download options.</p> <p class="text-gray-50 mb-4">Define a filter to be pre-applied to your download options.</p>
<div id="filter">
{{ form_start(preferences_form) }} {{ form_start(preferences_form) }}
{{ form_row(preferences_form.language) }} <div class="flex flex-col md:flex-row gap-2">
{{ form_row(preferences_form.quality) }}
{{ form_row(preferences_form.provider) }}
{{ form_row(preferences_form.resolution) }} {{ form_row(preferences_form.resolution) }}
{{ form_row(preferences_form.codec) }} {{ form_row(preferences_form.codec) }}
<button class="submit-button">Save</button> {{ form_row(preferences_form.language) }}
{{ form_row(preferences_form.provider) }}
{{ form_row(preferences_form.quality) }}
<div class="self-end mb-4">
<twig:SubmitButton show_icon text="Save"/>
</div>
</div>
{{ form_end(preferences_form) }} {{ form_end(preferences_form) }}
</div>
</twig:Card> </twig:Card>
</div>
<div class="p-4 flex flex-col md:flex-row gap-2">
<twig:Card title="Download Preferences" class="w-full"> <twig:Card title="Download Preferences" class="w-full">
<p class="text-gray-50 mb-4">Change how your downloads are stored.</p> <p class="text-gray-50 mb-4">Change how your downloads are stored.</p>
<form id="download_preferences" class="flex flex-col" name="download_preferences" method="post" action="{{ path('app_save_download_preferences') }}"> <form id="download_preferences" class="flex flex-col" name="download_preferences" method="post" action="{{ path('app_save_download_preferences') }}">