Skip to main content

Media

Video Quality Menu code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/video-quality-menu.blade.php

@props([
    'qualities' => [],
    'player' => 'default',
    'label' => 'Quality',
    'selected' => null,
])

@php
    /*
    | Optional external quality / rendition switcher for <x-ui.video-player>.
    | Prefer the player's built-in `qualities` prop for an on-player overlay.
    | Each quality: label (e.g. 1080p), src, description?
    |
    | Selecting an option dispatches `video-quality` with the new `src`.
    |
    | Usage:
    |   <x-ui.video-quality-menu player="watch" selected="720p" :qualities="[
    |       ['label' => '1080p', 'src' => '...'],
    |       ['label' => '720p', 'src' => '...'],
    |   ]" />
    */
    $normalized = collect($qualities)->map(function ($quality, $index) {
        return [
            'id' => (string) ($quality['id'] ?? $quality['label'] ?? $index),
            'label' => $quality['label'] ?? 'Source',
            'src' => $quality['src'] ?? null,
            'description' => $quality['description'] ?? null,
        ];
    })->filter(fn ($quality) => filled($quality['src']))->values()->all();

    $initial = null;

    if (filled($selected)) {
        $match = collect($normalized)->first(
            fn (array $quality) => $quality['label'] === $selected || $quality['id'] === (string) $selected
        );
        $initial = $match['id'] ?? null;
    }

    $initial ??= $normalized[0]['id'] ?? null;
@endphp

<div
    x-data="{
        open: false,
        player: @js((string) $player),
        qualities: @js($normalized),
        selected: @js($initial),
        get current() {
            return this.qualities.find((item) => item.id === this.selected) ?? this.qualities[0] ?? null;
        },
        choose(quality) {
            this.selected = quality.id;
            this.open = false;
            this.$dispatch('video-quality', {
                player: this.player,
                src: quality.src,
                label: quality.label,
            });
        },
    }"
    @click.outside="open = false"
    @keydown.escape.window="open = false"
    {{ $attributes->merge(['class' => 'relative inline-flex']) }}
>
    <button
        type="button"
        @click="open = ! open"
        class="inline-flex h-9 items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800"
        :aria-expanded="open.toString()"
        aria-haspopup="listbox"
        aria-label="{{ $label }}"
    >
        <x-ui.icon name="sliders-horizontal" size="sm" class="text-gray-400" />
        <span x-text="current?.label || @js($label)"></span>
        <x-ui.icon name="caret-down" weight="bold" size="sm" class="text-gray-400" />
    </button>

    <div
        x-show="open"
        x-cloak
        x-transition.opacity.duration.150ms
        class="absolute end-0 z-40 mt-2 w-48 overflow-hidden rounded-xl border border-gray-200 bg-white p-1.5 shadow-lg ring-1 ring-black/5 dark:border-gray-800 dark:bg-gray-900 sm:w-56"
        role="listbox"
        :aria-label="@js($label)"
    >
        <template x-for="quality in qualities" :key="quality.id">
            <button
                type="button"
                role="option"
                class="flex w-full items-start gap-2 rounded-lg px-3 py-2 text-start text-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:hover:bg-gray-800"
                :class="selected === quality.id ? 'bg-brand-50 text-brand-700 dark:bg-brand-950/40 dark:text-brand-300' : 'text-gray-700 dark:text-gray-200'"
                :aria-selected="selected === quality.id"
                @click="choose(quality)"
            >
                <span class="min-w-0 flex-1">
                    <span class="block font-medium" x-text="quality.label"></span>
                    <span x-show="quality.description" x-cloak class="mt-0.5 block text-xs text-gray-500 dark:text-gray-400" x-text="quality.description"></span>
                </span>
                <x-ui.icon x-show="selected === quality.id" x-cloak name="check" weight="bold" size="sm" class="mt-0.5 text-brand-600 dark:text-brand-400" />
            </button>
        </template>
    </div>
</div>