-// Media Library functionality
+//==============================//
+// Media Library functionality //
+//============================//
+
export default (options = {}) => ({
filters: options.filters || {}, // Filters JSON array is passed in from HTML
- gridSelector: options.gridSelector || false, // CSS selector for main Isotope element
- filtersOpen: false,
+ filtersOpen: false, // Visibility of the filters overlay
activeTypeFilters: [],
activeThemeFilters: [],
- isotope: {},
- playerOpen: false,
- playerType: 'video', // Can be 'video' or 'audio'
- playerSrc: '',
- playerPoster: '',
- playerMIME: '',
+ isotope: {}, // Holds the Isotope instance
+ isotopeOptions: { // Options used for Isotope instantiation (https://isotope.metafizzy.co/options.html)
+ layoutMode: 'fitRows',
+ percentPosition: true,
+ },
+
+ playerOpen: false, // Visibility of the media player overlay
+ player: {}, // Holds the Plyr instance
+ playerOptions: { // Settings used for Plyr instantiation (https://github.com/sampotts/plyr#options)
+ debug: true,
+ },
+
+ /***********\
+ | Filtering |
+ \***********/
get themeFilterList() {
// Convert active theme filters array into class string for Isotope
return list.join();
},
- init() {
- if (!this.gridSelector) {
- console.warn('Error: gridSelector not defined for media grid');
- return false;
- }
-
+ initIsotope(element) {
// Initialise Isotope
- this.isotope = new Isotope(this.gridSelector, {
- layoutMode: 'fitRows',
- percentPosition: true,
- });
+ this.isotope = new Isotope(element, this.isotopeOptions);
// Update Isotope whenever active filters change
this.$watch('activeTypeFilters', () => this.applyFilters());
},
applyFilters() {
- console.log('applying filters', this.themeFilterList)
- this.isotope.arrange({ filter: this.themeFilterList })
+ //console.log('applying filters', this.themeFilterList);
+ this.isotope.arrange({ filter: this.themeFilterList });
},
removeTypeFilter(filterID) {
// Remove the filter from the array
- this.activeTypeFilters = this.activeTypeFilters.filter(item => item !== filterID)
+ this.activeTypeFilters = this.activeTypeFilters.filter(item => item !== filterID);
},
removeThemeFilter(filterID) {
// Remove the filter from the array
- this.activeThemeFilters = this.activeThemeFilters.filter(item => item !== filterID)
+ this.activeThemeFilters = this.activeThemeFilters.filter(item => item !== filterID);
+ },
+
+ /**************\
+ | Media Player |
+ \**************/
+
+ initPlayer(selector) {
+
+ // Get custom controls HTML from the <template> tag
+ this.playerOptions.controls = document.getElementById('playerControls').innerHTML;
+
+ this.player = new Plyr(selector, this.playerOptions);
+ window.player = this.player; // Make player object available in console
+
+ this.player.on('timeupdate', event => {
+ const instance = event.detail.plyr;
+ console.log('timeupdate', instance.currentTime);
+ });
+ },
+
+ openPlayer(sourceData) {
+ this.playerOpen = true;
+ this.player.source = sourceData;
+ this.player.play();
+ },
+
+ closePlayer() {
+ this.player.stop();
+ this.playerOpen = false;
},
});
@endpush
{{-- See media_library() definition in resources/js/media-library.js --}}
-<div x-data="media_library({ gridSelector: '.media-grid', filters: {{ json_encode($filters) }} })">
+<div x-data="media_library({ filters: {{ json_encode($filters) }} })">
<x-media-library.filter-list />
{{-- MEDIA LIBRARY GRID --}}
{{-- Negative margins applied here to offset margins used in Isotope grid --}}
- <div class="media-grid mt-10 -mb-16 -mx-2.5">
+ <div class="media-grid mt-10 -mb-16 -mx-2.5" x-init="initIsotope($el)">
@foreach ($media as $item)
- <div @click="playerOpen = true;
- playerType = '{{ $item['type'] }}';
- playerSrc = '{{ $item['file'] }}';
- playerPoster = '{{ $item['image'] }}';
- playerMIME = '{{ $item['mime_type'] }}'"
+ <div @click="openPlayer({
+ type: '{{ $item['type'] }}',
+ //poster: '{{ $item['image'] }}',
+ sources: [{
+ src: '{{ $item['file'] }}',
+ type: '{{ $item['mime_type'] }}'
+ }]
+ })"
class="media-item
{{-- Width is 25% minus the gutters (2 * 0.625rem that comes from mx-2.5) --}}
float-left w-[calc(25%-1.25rem)] mx-2.5 mb-16
media-{{ $item['type'] }}
- theme-{{ $item['theme']['id'] }}">
+ theme-{{ $item['theme']['id'] }}"
+ >
<div class="media-item-image relative bg-cover bg-no-repeat rounded-md pb-[56.25%]" style="background-image:url({{ $item['image'] }})">
{{-- Play Icon --}}
</g>
</svg>
- <x-duration class="absolute bottom-2 right-2 font-secondary text-white">{{ $item['duration'] }}</x-duration>
+ <x-duration class="absolute bottom-2 right-2 font-secondary text-white"
+ style="text-shadow: rgb(0 0 0 / 50%) 0 0.1em 0.2em">
+ {{ $item['duration'] }}
+ </x-duration>
</div>
{{-- THEME LABEL --}}
<div class="mt-2.5 font-secondary font-medium text-xs leading-none" style="color:{{ $item['theme']['color'] }}">
x-transition:leave-end="opacity-0"
x-cloak>
- <x-close @click.prevent="playerOpen = false; playerType = '';" />
+ <x-close @click.prevent="closePlayer()" />
- <div class="w-full">
+ <div class="w-full" x-init="initPlayer('#player')">
+ {{-- Plyr placeholder --}}
+ {{--
+ Originally this should have been initialised using a simple div (non progressively enhanced method)
+ because the type of player (video or audio) is determined when opening the player and setting the source.
+ However, this doesn't work correctly, so we need to put a placeholder video tag that we can use to
+ initialise the Plyr. The source setter automatically handles switching between video and audio modes.
+ Ref: https://github.com/sampotts/plyr#the-source-setter
- <template x-if="playerType === 'video'">
- <video class="w-full" x-init="plyr = new Plyr($el)"
- {{-- Must call load() on video when changing the source
- (ref: https://github.com/alpinejs/alpine/discussions/2180 --}}
- x-effect="() => playerSrc && $el.load()"
- playsinline autoplay controls :data-poster="playerPoster">
- <source :src="playerSrc" :type="playerMIME">
- </video>
- </template>
+ For the initialisation, I wanted to use the same pattern as for Isotope, ie. x-init="initPlayer($el)" but
+ this fires the initialisation multiple times for some reason. It doesn't seem to be AlpineJS firing it
+ multiple times but I also don't see why Plyr would behave this way when passing a HTMLElement to it.
+ Instead, initialisation is done via ID, called on the wrapping element above: initPlayer('#player')
+ --}}
+ <video id="player" playsinline controls></video>
+ </div>
- <template x-if="playerType === 'audio'">
- <audio id="player" controls autoplay>
- <source :src="playerSrc" :type="playerMIME">
- </audio>
- </template>
+ {{-- Custom player controls: used when initialising the Plyr --}}
+ {{-- Ref: https://github.com/sampotts/plyr/blob/master/CONTROLS.md --}}
+ <template id="playerControls">
+ <div class="plyr__controls">
+ <div class="w-full">
+
+ <div class="plyr__progress">
+ <input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" aria-label="Seek">
+ <progress class="plyr__progress__buffer" min="0" max="100" value="0">% buffered</progress>
+ <span role="tooltip" class="plyr__tooltip">00:00</span>
+ </div>
+
+ <div class="flex justify-between items-center">
+
+ {{-- Left side --}}
+ <div class="flex items-center">
+ {{-- Play / Pause button --}}
+ <button type="button" class="plyr__control" aria-label="Play, {title}" data-plyr="play">
+ <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg>
+ <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg>
+ <span class="label--pressed plyr__tooltip" role="tooltip">Pause</span>
+ <span class="label--not-pressed plyr__tooltip" role="tooltip">Play</span>
+ </button>
+
+ {{-- Volume Mute --}}
+ <button type="button" class="plyr__control" aria-label="Mute" data-plyr="mute">
+ <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
+ <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
+ <span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
+ <span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span>
+ </button>
+
+ {{-- Volume slider --}}
+ <div class="plyr__volume">
+ <input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" aria-label="Volume">
+ </div>
+ </div>
+
+ {{-- Right side --}}
+ <div class="flex items-center">
+ {{-- Time codes --}}
+ <div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
+ <div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
+ </div>
+
+ </div>
+
+ {{-- Fullscreen --}}
+ {{--
+ <button type="button" class="plyr__control" data-plyr="fullscreen">
+ <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
+ <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
+ <span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>
+ <span class="label--not-pressed plyr__tooltip" role="tooltip">Enter fullscreen</span>
+ </button>
+ --}}
+ </div>
+
+ </div>
+ </template>
- </div>
</div>