fix: working multi-choice filtering

This commit is contained in:
2025-07-26 10:20:37 -05:00
parent 87e72ec55e
commit f9ec089f8b
10 changed files with 216 additions and 75 deletions

View File

@@ -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));
}
}

View File

@@ -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 = '<option value="'+preferred+'" selected>'+preferred+'</option>';
this.languageTarget.innerHTML += '<option value="">n/a</option>';
} else {
this.languageTarget.innerHTML = '<option value="">n/a</option>';
}
this.languageTarget.innerHTML = '<option value="">n/a</option>';
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 '<option value="'+language+'" selected>'+language+'</option>';
}
return '<option value="'+language+'">'+language+'</option>';
})
@@ -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)
}
}

View File

@@ -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;
}

View File

@@ -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": {

View File

@@ -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()),
);
}
}

View File

@@ -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,
) {}
}

View File

@@ -27,6 +27,7 @@ class UserPreferencesFactory
if ($value === "") {
return null;
}
$value = explode(',', $value);
return $value;
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -7,69 +7,109 @@
data-action="change->result-filter#filter action-button:downloadSeason@window->result-filter#downloadSeason"
>
<div class="w-full p-4 flex flex-col md:flex-row gap-4 bg-stone-500 text-md text-gray-500 dark:text-gray-50 rounded-lg">
<label for="resolution">
<label for="resolution" class="flex flex-col gap-1">
Resolution
<select id="resolution"
multiple="multiple"
data-result-filter-target="resolution"
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
value="{{ app.user.userPreferenceValues["resolution"] }}"
class="px-1 py-0.5 bg-stone-100 placeholder-text-gray-50 text-gray-50"
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
create: false,
tomSelectOptions: {
highlight: false,
}
}) }}
>
<option value="">n/a</option>
{% for name, value in this.resolutionOptions %}
<option value="{{ value }}"
{{ value == this.userPreferences['resolution'] ? 'selected' }}
{{ value in this.userPreferences['resolution'] ? 'selected' }}
>{{ name }}</option>
{% endfor %}
</select>
</label>
<label for="codec">
<label for="codec" class="flex flex-col gap-1">
Codec
<select id="codec" data-result-filter-target="codec" class="px-1 py-0.5 bg-stone-100 text-sm text-gray-800 rounded-md">
<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 == this.userPreferences['codec'] ? 'selected' }}
{{ value in this.userPreferences['codec'] ? 'selected' }}
>{{ name }}</option>
{% endfor %}
</select>
</label>
<label for="language">
<label for="language" class="flex flex-col gap-1">
Language
<select id="language"
multiple="multiple"
data-result-filter-target="language"
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
{{ 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'] }}"
data-preferred="{{ this.userPreferences['language']|json_encode }}"
{% endif %}
>
</select>
</label>
<label for="provider">
<label for="provider" class="flex flex-col gap-1">
Provider
<select id="provider"
multiple="multiple"
data-result-filter-target="provider"
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
{{ 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'] }}"
data-preferred="{{ this.userPreferences['provider']|json_encode }}"
{% endif %}
>
</select>
</label>
<label for="quality">
<label for="quality" class="flex flex-col gap-1">
Quality
<select id="quality"
multiple="multiple"
data-result-filter-target="quality"
class="px-1 py-0.5 bg-stone-100 text-gray-800 rounded-md"
class="px-1 py-0.5 bg-stone-100 text-gray-50"
{{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
create: false,
tomSelectOptions: {
highlight: false,
}
}) }}
{% if this.userPreferences['quality'] != null %}
data-preferred="{{ this.userPreferences['quality'] }}"
data-preferred="{{ this.userPreferences['quality']|json_encode }}"
{% endif %}
>
</select>
</label>
{% if results.media.mediaType == "tvshows" %}
<label for="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 rounded-md"
<select id="season" name="season" value="1" data-result-filter-target="season" class="px-1 py-0.5 bg-stone-100 text-gray-800"
{{ stimulus_action('result_filter', 'setSeason', 'change') }}
{{ stimulus_action('result_filter', 'uncheckSelectAllBtn', 'change') }}
>