From f9ec089f8be8a15b5e23cb1b8526bc6a1292d421 Mon Sep 17 00:00:00 2001 From: Brock H Caldwell Date: Sat, 26 Jul 2025 10:20:37 -0500 Subject: [PATCH] fix: working multi-choice filtering --- assets/components/download-option-tr.js | 120 ++++++++++++++---- .../controllers/result_filter_controller.js | 33 +++-- assets/styles/app.css | 35 +++++ composer.json | 1 + .../SaveUserMediaPreferencesCommand.php | 10 +- src/User/Dto/UserPreferences.php | 10 +- src/User/Dto/UserPreferencesFactory.php | 1 + .../Controller/Web/PreferencesController.php | 3 +- .../Form/UserMediaPreferencesForm.php | 4 + templates/components/Filter.html.twig | 74 ++++++++--- 10 files changed, 216 insertions(+), 75 deletions(-) diff --git a/assets/components/download-option-tr.js b/assets/components/download-option-tr.js index 7f5f5df..15c60aa 100644 --- a/assets/components/download-option-tr.js +++ b/assets/components/download-option-tr.js @@ -1,6 +1,15 @@ export default class DownloadOptionTr extends HTMLTableRowElement { - H264_CODECS = ['h264', 'h.264', 'x264'] - H265_CODECS = ['h265', 'h.265', 'x265', 'hevc'] + H264_CODECS = { + 'h264': 'h264', + 'h.264': 'h264', + 'x264': 'h264', + } + H265_CODECS = { + 'h265': 'h265', + 'h.265': 'h265', + 'x265': 'h265', + 'hevc': 'h265', + } #downloadBtnEl; #selectEpisodeInputEl; @@ -53,13 +62,6 @@ export default class DownloadOptionTr extends HTMLTableRowElement { filter({ detail: { activeFilter } }) { const optionHeader = document.querySelector(`[data-option-id="${this.dataset['localId']}"]`) - const props = { - "resolution": this.resolution.trim(), - "codec": this.codec.trim(), - "provider": this.provider.trim(), - "languages": this.languages, - "quality": this.quality, - } let include = true; this.classList.add('r-tablerow'); @@ -69,25 +71,24 @@ export default class DownloadOptionTr extends HTMLTableRowElement { this.querySelector('input[type="checkbox"]').checked = false; - for (let [key, value] of Object.entries(activeFilter)) { - if (value === "" || key === "season") { - continue; - } - if (key === "codec" && value === "h264") { - if (!this.H264_CODECS.includes(props[key].toLowerCase())) { - include = false; - } - } else if (key === "codec" && value === "h265") { - if (!this.H265_CODECS.includes(props[key].toLowerCase())) { - include = false; - } - } else if (key === "language") { - if (!props["languages"].includes(value)) { - include = false; - } - } else if (props[key] !== value) { - include = false; - } + if (!this.#validateResolutions(activeFilter.resolution)) { + include = false; + } + + if (!this.#validateCodecs(activeFilter.codec)) { + include = false; + } + + if (!this.#validateLanguages(activeFilter.language)) { + include = false; + } + + if (!this.#validateQualities(activeFilter.quality)) { + include = false; + } + + if (!this.#validateProviders(activeFilter.provider)) { + include = false; } if (false === include) { @@ -121,4 +122,67 @@ export default class DownloadOptionTr extends HTMLTableRowElement { console.log(json) }) } + + #validateResolutions(selectedOptions) { + return this.#validateIntersection(selectedOptions, this.resolution.trim().split(',')); + } + + #validateCodecs(selectedOptions) { + if (this.#validateIntersection(selectedOptions, Object.keys(this.H264_CODECS))) { + return this.#validateIntersection( + selectedOptions, + [...this.codec.trim().split(','), '', 'n/a'] + ); + } + if (this.#validateIntersection(selectedOptions, Object.keys(this.H265_CODECS))) { + return this.#validateIntersection( + selectedOptions, + [...this.codec.trim().split(','), '', 'n/a'] + ); + } + return false; + } + + #validateQualities(selectedOptions) { + return this.#validateIntersection(selectedOptions, this.quality.trim().split(',')); + } + + #validateProviders(selectedOptions) { + return this.#validateIntersection(selectedOptions, this.provider.trim().split(',')); + } + + #validateLanguages(selectedOptions) { + return this.#validateIntersection(selectedOptions, this.languages); + } + + #validateIntersection(selectedOptions, localOptions) { + if (selectedOptions === null || selectedOptions === undefined) { + return true; + } + + if (typeof selectedOptions === 'string' || selectedOptions instanceof String) { + selectedOptions = [selectedOptions]; + } + + if (selectedOptions.length === 0 || + (selectedOptions.length === 1 && selectedOptions[0] === "") || + (selectedOptions.length === 1 && selectedOptions[0] === "-") || + (selectedOptions.length === 1 && selectedOptions[0] === "n/a") + ) { + return true; + } + + return this.#doesIntersect(localOptions, selectedOptions); + } + + #doesIntersect(a, b) { + if (a.length === 0 || b.length === 0) { + return false; + } + return this.#intersect(a, b).length > 0; + } + + #intersect(a, b) { + return a.filter(Set.prototype.has, new Set(b)); + } } diff --git a/assets/controllers/result_filter_controller.js b/assets/controllers/result_filter_controller.js index a529ee7..6de0e4b 100644 --- a/assets/controllers/result_filter_controller.js +++ b/assets/controllers/result_filter_controller.js @@ -60,26 +60,18 @@ export default class extends Controller { } addLanguages(option) { - const languages = Object.assign([], option.languages); - languages.forEach((language) => { + option.languages.forEach((language) => { if (!this.languages.includes(language)) { this.languages.push(language); } }); - const preferred = this.languageTarget.dataset.preferred; - if (preferred) { - this.languageTarget.innerHTML = ''; - this.languageTarget.innerHTML += ''; - } else { - this.languageTarget.innerHTML = ''; - } - + this.languageTarget.innerHTML = ''; this.languageTarget.innerHTML += this.languages.sort() .map((language) => { - const preferred = this.languageTarget.dataset.preferred; - if (preferred === language) { - return; + const preferred = this.languageTarget.dataset.preferred.split(','); + if (preferred.includes(language.toLowerCase())) { + return ''; } return ''; }) @@ -141,11 +133,11 @@ export default class extends Controller { const downloadSeasonSpan = document.querySelector("#downloadSeasonModal"); this.activeFilter = { - "resolution": this.resolutionTarget.value, - "codec": this.codecTarget.value, - "language": this.languageTarget.value, - "provider": this.providerTarget.value, - "quality": this.qualityTarget.value, + "resolution": this.#fetchValuesFromNodeList(this.resolutionTarget.selectedOptions), + "codec": this.#fetchValuesFromNodeList(this.codecTarget.selectedOptions), + "language": this.#fetchValuesFromNodeList(this.languageTarget.selectedOptions), + "provider": this.#fetchValuesFromNodeList(this.providerTarget.selectedOptions), + "quality": this.#fetchValuesFromNodeList(this.qualityTarget.selectedOptions), } if ("tvshows" === this.mediaTypeValue) { @@ -175,4 +167,9 @@ export default class extends Controller { } }) } + + #fetchValuesFromNodeList(nodeList) { + console.log([...nodeList].map(option => option.value)) + return [...nodeList].map(option => option.value) + } } diff --git a/assets/styles/app.css b/assets/styles/app.css index 478b4d6..ad2628a 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -68,6 +68,16 @@ dialog[data-dialog-target="dialog"][closing] { @apply bg-gray-50 text-gray-50 px-2 py-1 bg-transparent border-b-2 border-orange-400 } +.text-input[multiple="multiple"] { + @apply bg-transparent backdrop-filter backdrop-blur-md text-white border border-orange-500 rounded-md +} + +.text-input option[checked="checked"], +.text-input option[checked], +.text-input option[selected] { + @apply bg-orange-500/60 +} + .submit-button { @apply bg-green-600/40 px-1.5 py-1 w-full rounded-md text-gray-50 backdrop-filter backdrop-blur-sm border-2 border-green-500 hover:bg-green-700/40 } @@ -148,3 +158,28 @@ dialog[data-dialog-target="dialog"][closing] { z-index: 2; background: transparent; } + +#filter { + .ts-wrapper { + box-shadow: none !important; + padding: 0; + + .ts-control { + background: transparent !important; + border: none !important; + box-shadow: none !important; + } + + .item[data-ts-item] { + background-image: none !important; + border: none; + @apply bg-orange-500; + } + + @apply border-b-2 border-b-orange-600 bg-transparent; + } +} + +.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove { + @apply border-l-orange-800; +} diff --git a/composer.json b/composer.json index 17d2d7f..b9f1674 100644 --- a/composer.json +++ b/composer.json @@ -104,6 +104,7 @@ "post-update-cmd": [ "@auto-scripts" ], + "tail": "docker compose exec app ./bin/console tailwind:build --watch", "sym": "docker compose exec app ./bin/console" }, "conflict": { diff --git a/src/User/Action/Command/SaveUserMediaPreferencesCommand.php b/src/User/Action/Command/SaveUserMediaPreferencesCommand.php index b084877..cc9b121 100644 --- a/src/User/Action/Command/SaveUserMediaPreferencesCommand.php +++ b/src/User/Action/Command/SaveUserMediaPreferencesCommand.php @@ -19,11 +19,11 @@ class SaveUserMediaPreferencesCommand implements CommandInterface public static function fromUserMediaPreferencesForm(FormInterface $form): self { return new static( - resolution: $form->get('resolution')->getData(), - codec: $form->get('codec')->getData(), - quality: $form->get('quality')->getData(), - language: $form->get('language')->getData(), - provider: $form->get('provider')->getData(), + resolution: \implode(',', $form->get('resolution')->getData()), + codec: \implode(',', $form->get('codec')->getData()), + quality: \implode(',', $form->get('quality')->getData()), + language: \implode(',', $form->get('language')->getData()), + provider: \implode(',', $form->get('provider')->getData()), ); } } \ No newline at end of file diff --git a/src/User/Dto/UserPreferences.php b/src/User/Dto/UserPreferences.php index 3b6bf77..c775fe8 100644 --- a/src/User/Dto/UserPreferences.php +++ b/src/User/Dto/UserPreferences.php @@ -6,10 +6,10 @@ class UserPreferences { public function __construct( - public readonly ?string $resolution, - public readonly ?string $codec, - public readonly ?string $language, - public readonly ?string $provider, - public readonly ?string $quality, + public readonly ?array $resolution, + public readonly ?array $codec, + public readonly ?array $language, + public readonly ?array $provider, + public readonly ?array $quality, ) {} } diff --git a/src/User/Dto/UserPreferencesFactory.php b/src/User/Dto/UserPreferencesFactory.php index 6a93f7c..9af72a3 100644 --- a/src/User/Dto/UserPreferencesFactory.php +++ b/src/User/Dto/UserPreferencesFactory.php @@ -27,6 +27,7 @@ class UserPreferencesFactory if ($value === "") { return null; } + $value = explode(',', $value); return $value; } } diff --git a/src/User/Framework/Controller/Web/PreferencesController.php b/src/User/Framework/Controller/Web/PreferencesController.php index a5a93be..87a0f64 100644 --- a/src/User/Framework/Controller/Web/PreferencesController.php +++ b/src/User/Framework/Controller/Web/PreferencesController.php @@ -52,8 +52,7 @@ class PreferencesController extends AbstractController ): Response { $downloadPreferences = $this->getUser()->getDownloadPreferences(); - $formData = (array) UserPreferencesFactory::createFromUser($this->getUser()); - $form = $this->createForm(UserMediaPreferencesForm::class, $formData); + $form = $this->createForm(UserMediaPreferencesForm::class); $form->handleRequest($request); diff --git a/src/User/Framework/Form/UserMediaPreferencesForm.php b/src/User/Framework/Form/UserMediaPreferencesForm.php index b7b00c4..694e326 100644 --- a/src/User/Framework/Form/UserMediaPreferencesForm.php +++ b/src/User/Framework/Form/UserMediaPreferencesForm.php @@ -10,8 +10,11 @@ use App\User\Database\QualityList; use App\User\Database\ResolutionList; use App\User\Framework\Repository\PreferenceOptionRepository; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Event\PreSetDataEvent; +use Symfony\Component\Form\Event\PreSubmitEvent; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Routing\Generator\UrlGenerator; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -38,6 +41,7 @@ class UserMediaPreferencesForm extends AbstractType 'label_attr' => ['class' => 'w-64 text-white block font-semibold mb-2'], 'choices' => $this->addDefaultChoice($choices), 'required' => false, + 'multiple' => true, ]; $builder->add($fieldName, ChoiceType::class, $question); } diff --git a/templates/components/Filter.html.twig b/templates/components/Filter.html.twig index 378e25f..c16d1cf 100644 --- a/templates/components/Filter.html.twig +++ b/templates/components/Filter.html.twig @@ -7,69 +7,109 @@ data-action="change->result-filter#filter action-button:downloadSeason@window->result-filter#downloadSeason" >
-