Media
Cast Share Sheet code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/cast-share-sheet.blade.php
@props([
'url',
'title' => null,
'player' => 'default',
'startAt' => null,
'embed' => null,
'label' => 'Share',
'download' => null,
'cast' => true,
])
@php
/*
| Expanded share sheet: copy link / timestamp / embed, native share,
| download, and cast affordances. Syncs time from `video-time`.
|
| Usage:
| <x-ui.cast-share-sheet
| url="https://example.com/videos/1"
| title="Spring bloom"
| player="watch"
| download="/videos/1.mp4"
| />
*/
@endphp
<div
x-data="{
open: false,
player: @js((string) $player),
url: @js((string) $url),
title: @js($title),
embed: @js($embed),
download: @js($download),
castEnabled: @js((bool) $cast),
current: @js(is_numeric($startAt) ? (float) $startAt : 0),
copied: null,
castMessage: null,
timer: null,
get timestampUrl() {
const base = this.url.split('#')[0];
const seconds = Math.max(0, Math.floor(this.current || 0));
return `${base}#t=${seconds}`;
},
formatTime(seconds) {
const total = Math.max(0, Math.floor(seconds || 0));
const m = Math.floor(total / 60);
const s = String(total % 60).padStart(2, '0');
return `${m}:${s}`;
},
async copy(value, key) {
try {
await navigator.clipboard.writeText(value);
} catch (error) {
const area = document.createElement('textarea');
area.value = value;
area.setAttribute('readonly', '');
area.style.position = 'fixed';
area.style.left = '-9999px';
document.body.appendChild(area);
area.select();
document.execCommand('copy');
document.body.removeChild(area);
}
this.copied = key;
clearTimeout(this.timer);
this.timer = setTimeout(() => { this.copied = null; }, 1600);
},
async nativeShare() {
if (! navigator.share) {
await this.copy(this.url, 'share');
return;
}
try {
await navigator.share({
title: this.title || document.title,
url: this.timestampUrl,
});
} catch (error) {
// User cancelled — ignore.
}
},
requestCast(target) {
this.castMessage = target === 'airplay'
? 'AirPlay is available from the system media controls when supported.'
: 'Chromecast devices appear in your browser cast menu when available.';
this.$dispatch('media-cast', { player: this.player, target });
clearTimeout(this.timer);
this.timer = setTimeout(() => { this.castMessage = null; }, 2800);
},
onTime(detail) {
if (detail?.player && detail.player !== this.player) { return; }
this.current = Number(detail?.time ?? this.current);
},
}"
@video-time.window="onTime($event.detail)"
@click.outside="open = false"
@keydown.escape.window="open = false"
{{ $attributes->merge(['class' => 'relative inline-flex max-w-full']) }}
>
<button
type="button"
@click="open = ! open"
class="inline-flex h-10 max-w-full 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 sm:h-9"
:aria-expanded="open.toString()"
aria-haspopup="dialog"
>
<x-ui.icon name="share-network" size="sm" class="shrink-0" />
<span class="truncate">{{ $label }}</span>
</button>
<div
x-show="open"
x-cloak
x-transition.opacity.duration.150ms
class="absolute end-0 z-40 mt-2 w-[min(100vw-2rem,22rem)] overflow-hidden rounded-2xl border border-gray-200 bg-white p-4 shadow-lg ring-1 ring-black/5 dark:border-gray-800 dark:bg-gray-900"
role="dialog"
aria-label="{{ $label }}"
>
<div class="mb-3">
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ $label }}</p>
@if (filled($title))
<p class="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400">{{ $title }}</p>
@endif
</div>
<div class="space-y-2">
<button type="button" class="flex w-full items-center justify-between gap-3 rounded-xl border border-gray-200 px-3 py-2.5 text-start text-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:hover:bg-gray-800" @click="copy(url, 'link')">
<span class="min-w-0">
<span class="block font-medium text-gray-900 dark:text-white">Copy link</span>
<span class="mt-0.5 block truncate text-xs text-gray-500 dark:text-gray-400" x-text="url"></span>
</span>
<span class="shrink-0 text-xs font-semibold text-brand-600 dark:text-brand-400" x-text="copied === 'link' ? 'Copied' : 'Copy'"></span>
</button>
<button type="button" class="flex w-full items-center justify-between gap-3 rounded-xl border border-gray-200 px-3 py-2.5 text-start text-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:hover:bg-gray-800" @click="copy(timestampUrl, 'time')">
<span class="min-w-0">
<span class="block font-medium text-gray-900 dark:text-white">Copy at current time</span>
<span class="mt-0.5 block text-xs text-gray-500 dark:text-gray-400">Starts at <span x-text="formatTime(current)"></span></span>
</span>
<span class="shrink-0 text-xs font-semibold text-brand-600 dark:text-brand-400" x-text="copied === 'time' ? 'Copied' : 'Copy'"></span>
</button>
<template x-if="embed">
<button type="button" class="flex w-full items-center justify-between gap-3 rounded-xl border border-gray-200 px-3 py-2.5 text-start text-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:hover:bg-gray-800" @click="copy(embed, 'embed')">
<span class="min-w-0">
<span class="block font-medium text-gray-900 dark:text-white">Copy embed code</span>
<span class="mt-0.5 block text-xs text-gray-500 dark:text-gray-400">iframe snippet</span>
</span>
<span class="shrink-0 text-xs font-semibold text-brand-600 dark:text-brand-400" x-text="copied === 'embed' ? 'Copied' : 'Copy'"></span>
</button>
</template>
<button type="button" class="flex w-full items-center justify-between gap-3 rounded-xl border border-gray-200 px-3 py-2.5 text-start text-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:hover:bg-gray-800" @click="nativeShare()">
<span class="min-w-0">
<span class="block font-medium text-gray-900 dark:text-white">Share via device</span>
<span class="mt-0.5 block text-xs text-gray-500 dark:text-gray-400">Uses the system share sheet when available</span>
</span>
<x-ui.icon name="export" size="sm" class="shrink-0 text-gray-400" />
</button>
<template x-if="download">
<a
:href="download"
class="flex w-full items-center justify-between gap-3 rounded-xl border border-gray-200 px-3 py-2.5 text-start text-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:hover:bg-gray-800"
download
>
<span class="min-w-0">
<span class="block font-medium text-gray-900 dark:text-white">Download</span>
<span class="mt-0.5 block text-xs text-gray-500 dark:text-gray-400">Save the original file</span>
</span>
<x-ui.icon name="download-simple" size="sm" class="shrink-0 text-gray-400" />
</a>
</template>
<template x-if="castEnabled">
<div class="grid grid-cols-2 gap-2 pt-1">
<button type="button" class="inline-flex h-10 items-center justify-center gap-2 rounded-xl border border-gray-200 text-sm font-medium text-gray-700 transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:text-gray-200 dark:hover:bg-gray-800" @click="requestCast('chromecast')">
<x-ui.icon name="broadcast" size="sm" />
Cast
</button>
<button type="button" class="inline-flex h-10 items-center justify-center gap-2 rounded-xl border border-gray-200 text-sm font-medium text-gray-700 transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:text-gray-200 dark:hover:bg-gray-800" @click="requestCast('airplay')">
<x-ui.icon name="airplay" size="sm" />
AirPlay
</button>
</div>
</template>
<p x-show="castMessage" x-cloak x-text="castMessage" class="text-xs leading-5 text-gray-500 dark:text-gray-400"></p>
</div>
</div>
</div>