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>