Skip to main content

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>