Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"league/glide": "^3.0 || ^4.0",
"maennchen/zipstream-php": "^3.1",
"michelf/php-smartypants": "^1.8.1",
"mpratt/embera": "^2.0.40",
"nesbot/carbon": "^3.0",
"pragmarx/google2fa": "^8.0 || ^9.0",
"rebing/graphql-laravel": "^9.15",
Expand Down
138 changes: 100 additions & 38 deletions resources/js/components/fieldtypes/VideoFieldtype.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
<template>
<div class="flex flex-col space-y-3 p-1.5 bg-gray-100 border border-gray-300 dark:bg-gray-900 dark:border-gray-700 rounded-xl">
<ui-input-group>
<ui-combobox
:model-value="mode"
:options="meta.providers"
option-label="label"
option-value="value"
:aria-label="__('Video Provider')"
@update:model-value="changeMode"
/>
<ui-input-group v-if="isCloudflare">
<ui-input-group-prepend :text="__('ID')" />
<ui-input
:model-value="videoId"
:isReadOnly="isReadOnly"
:aria-label="__('Video ID')"
@update:model-value="updateCloudflareId"
@focus="$emit('focus')"
@blur="$emit('blur')"
input-class="border-s-0"
/>
</ui-input-group>
<ui-input-group v-else>
<ui-input-group-prepend :text="__('URL')" />
<ui-input
:model-value="value"
Expand All @@ -14,8 +34,14 @@
/>
</ui-input-group>
<ui-description v-if="isInvalid" class="text-red-600">{{ __('statamic::validation.url') }}</ui-description>
<video
v-if="shouldShowPreview && isFile"
:src="isVisible ? embedUrl : null"
controls
class="aspect-video rounded-lg w-full"
></video>
<iframe
v-if="shouldShowPreview"
v-else-if="shouldShowPreview"
ref="iframe"
:src="isVisible ? embedUrl : null"
frameborder="0"
Expand All @@ -27,73 +53,105 @@
</template>

<script>
import axios from 'axios';
import Fieldtype from './Fieldtype.vue';

const CLOUDFLARE = 'cloudflare';
const CLOUDFLARE_PREFIX = 'cloudflare:';
const FILE = 'file';
const UNSUPPORTED = 'unsupported';
const URL_MODE = 'url';

export default {
mixins: [Fieldtype],

data() {
return {
abortController: null,
isVisible: false,
lookedUp: this.meta.video,
observer: null,
// Only consulted when there's no value; otherwise the value itself says which input to show.
mode: this.meta.video?.provider === CLOUDFLARE ? CLOUDFLARE : URL_MODE,
};
},

computed: {
shouldShowPreview() {
return !this.isInvalid && (this.isEmbeddable || this.isVideo);
return !this.isInvalid && !!this.embedUrl;
},

embedUrl() {
let embed_url = this.value || '';
isFile() {
return this.lookedUp?.url === this.value && this.lookedUp.provider === FILE;
},

if (embed_url.includes('youtube')) {
embed_url = embed_url.includes('shorts/')
? embed_url.replace('shorts/', 'embed/')
: embed_url.replace('watch?v=', 'embed/');
embedUrl() {
if (this.isCloudflare) {
return this.videoId ? `https://iframe.cloudflarestream.com/${this.videoId}` : null;
}

if (embed_url.includes('youtu.be')) {
embed_url = embed_url.replace('youtu.be', 'www.youtube.com/embed');
}
return this.lookedUp?.url === this.value ? this.lookedUp.embed_url : null;
},

if (embed_url.includes('vimeo')) {
embed_url = embed_url.replace('/vimeo.com', '/player.vimeo.com/video');
isCloudflare() {
return this.value?.startsWith(CLOUDFLARE_PREFIX) || (!this.value && this.mode === CLOUDFLARE);
},

if (!this.value.includes('progressive_redirect') && embed_url.split('/').length > 5) {
let hash = embed_url.substr(embed_url.lastIndexOf('/') + 1);
embed_url = embed_url.substr(0, embed_url.lastIndexOf('/')) + '?h=' + hash.replace('?', '&');
}
}
isInvalid() {
if (this.isCloudflare) return !!this.videoId && !/^[a-zA-Z0-9]+$/.test(this.videoId);

if (embed_url.includes('&') && !embed_url.includes('?')) {
embed_url = embed_url.replace('&', '?');
}
return !!this.value && this.lookedUp?.url === this.value && this.lookedUp.provider === UNSUPPORTED;
},

return embed_url;
videoId() {
return this.value?.startsWith(CLOUDFLARE_PREFIX) ? this.value.slice(CLOUDFLARE_PREFIX.length) : null;
},
},

watch: {
value: {
handler(value) {
if (value?.startsWith(CLOUDFLARE_PREFIX) || !value) return;

isEmbeddable() {
const url = this.value || '';
const isYoutube = url.includes('youtube') || url.includes('youtu.be');
const isVimeo = url.includes('vimeo');
return isYoutube || isVimeo;
this.lookup(value);
},
immediate: true,
},
},

isInvalid() {
let htmlRegex = new RegExp(/<([A-Z][A-Z0-9]*)\b[^>]*>.*?<\/\1>|<([A-Z][A-Z0-9]*)\b[^\/]*\/>/i);
return htmlRegex.test(this.value || '');
methods: {
changeMode(mode) {
if (mode === this.mode) return;

this.mode = mode;

if (this.value) this.update(null);
},

isUrl() {
const url = this.value || '';
return url.startsWith('http://') || url.startsWith('https://');
lookup(value) {
if (this.lookedUp?.url === value) return;

if (this.abortController) this.abortController.abort();

this.abortController = new AbortController();

this.$axios
.get(this.meta.detailsUrl, { params: { value }, signal: this.abortController.signal })
.then((response) => {
// Ignore a response that arrived after the value moved on.
if (value === this.value) this.lookedUp = response.data;
})
.catch((e) => {
if (axios.isCancel(e)) return;
if (value !== this.value) return;

this.lookedUp = null;
this.$toast.error(e.response ? e.response.data.message : __('Something went wrong'));
});
},

isVideo() {
const url = this.value || '';
const isVideo = url.includes('.mp4') || url.includes('.ogv') || url.includes('.mov') || url.includes('.webm');
return !this.isEmbeddable && isVideo;
updateCloudflareId(id) {
this.update(id ? `${CLOUDFLARE_PREFIX}${id}` : null);
},
},

Expand All @@ -119,6 +177,10 @@ export default {
if (this.observer) {
this.observer.disconnect();
}

if (this.abortController) {
this.abortController.abort();
}
},
};
</script>
Loading
Loading