video picker

This commit is contained in:
2022-01-19 13:53:33 +01:00
parent 33bf8f6448
commit 23aeec3ceb
12 changed files with 533 additions and 17 deletions
@@ -1,5 +1,5 @@
<template>
<ui-toggle :on="value" @update:on="$emit('input', $event)" :disabled="config.disabled" v-bind="{ negative, onContent, offContent }" />
<ui-toggle :on="value" @update:on="$emit('input', $event)" :disabled="config.disabled" v-bind="{ negative, onContent, offContent }" :content-left="true" />
</template>
@@ -0,0 +1,20 @@
<template>
<ui-videopicker :value="value"
@input="$emit('input', $event)"
:limit="limit"
:disabled="config.disabled" />
</template>
<script>
export default {
props: {
value: [Object, Array],
config: Object,
limit: {
type: Number,
default: 1
}
}
}
</script>
@@ -0,0 +1,171 @@
<template>
<div class="shop-videopicker" :class="{'is-disabled': disabled }">
<input ref="input" type="hidden" :value="value" />
<div class="shop-videopicker-previews" v-if="previews.length > 0" v-sortable="{ onUpdate: onSortingUpdated }">
<div v-for="(preview, index) in previews" :key="index" class="shop-videopicker-preview">
<ui-select-button :icon="preview.icon" :label="preview.name" :description="preview.text" :disabled="disabled" @click="pick(preview.model)" :tokens="{ id: preview.id }" />
<ui-icon-button v-if="!disabled" @click="remove(preview.model)" icon="fth-x" title="@ui.close" />
</div>
</div>
<ui-select-button v-if="canAdd" icon="fth-plus" :label="limit > 1 ? '@ui.add' : '@ui.select'" @click="pick()" :disabled="disabled" />
</div>
</template>
<script>
import * as overlays from '../../../services/overlay';
export default {
name: 'uiVideopicker',
props: {
value: {
type: [Object, Array],
default: null
},
limit: {
type: Number,
default: 1
},
disabled: {
type: Boolean,
default: false
},
options: {
type: Object,
default: () => { }
}
},
data: () => ({
items: [],
previews: []
}),
watch: {
value(val)
{
this.setup(val);
this.updatePreviews();
}
},
computed: {
multiple()
{
return this.limit > 1;
},
canAdd()
{
let count = Array.isArray(this.value) ? this.value.length : (!this.value ? 0 : 1);
return !this.disabled && count < this.limit;
}
},
mounted()
{
this.setup(this.value);
this.updatePreviews();
},
methods: {
setup(value)
{
this.items = JSON.parse(JSON.stringify(value)) || [];
if (!this.multiple)
{
this.items = this.items && !Array.isArray(this.items) ? [this.items] : [];
}
console.info(this.items);
},
onChange()
{
let value = this.multiple ? [...this.items] : (this.items.length > 0 ? this.items[0] : null);
this.$emit('input', value);
this.$emit('update:value', value);
},
onSortingUpdated(ev)
{
this.previews = arrayMove(this.previews, ev.oldIndex, ev.newIndex);
this.items = arrayMove(this.items, ev.oldIndex, ev.newIndex);
this.onChange();
},
async updatePreviews()
{
this.previews = this.items.map((item, idx) => ({
id: idx,
name: '@videopicker.providers.' + item.provider,
text: item.title,
icon: 'fth-video',
model: item
}));
},
remove(item)
{
this.items.splice(this.items.indexOf(item), 1);
this.onChange();
},
async pick(item)
{
if (this.disabled)
{
return;
}
const result = await overlays.open({
component: () => import('../overlays/videopicker.vue'),
display: 'editor',
model: item
})
if (result.eventType == 'confirm')
{
if (item && item.id)
{
this.items.splice(this.items.indexOf(item), 1);
}
this.items.push(result.value);
this.onChange();
}
}
}
}
</script>
<style lang="scss">
.shop-videopicker-preview
{
display: flex;
justify-content: space-between;
align-items: center;
.ui-icon-button
{
height: 24px;
width: 24px;
i
{
font-size: 13px;
}
}
}
.shop-videopicker-previews + .ui-select-button,
.shop-videopicker-preview + .shop-videopicker-preview
{
margin-top: 10px;
}
</style>
@@ -0,0 +1,197 @@
<template>
<ui-trinity class="ui-videopicker-overlay">
<template v-slot:header>
<ui-header-bar title="@videopicker.headline" :back-button="false" :close-button="true" @close="config.close(true)" />
</template>
<template v-slot:footer>
<ui-button type="light onbg" label="@ui.close" :parent="config.rootId" @click="config.close" />
<ui-button type="primary" label="@ui.save" @click="onSave" :state="state" />
</template>
<div v-if="opened">
<div class="ui-box ui-videopicker-overlay-options">
<ui-property label="@videopicker.fields.provider" :vertical="true" :required="true">
<ui-select v-model="item.provider" :items="providers" />
</ui-property>
<ui-property v-if="item.provider != 'html'" label="@videopicker.fields.videoUrl" :vertical="true">
<input v-model="item.videoUrl" @input="onUrlChange($event.target.value)" type="text" class="ui-input" />
<p v-if="item.videoId" class="ui-property-help" v-localize:html="{ key: '@videopicker.fields.foundParsed', tokens: { id: item.videoId } }"></p>
</ui-property>
<ui-property v-if="item.provider == 'html'" label="@videopicker.fields.videoId" :required="true">
<ui-mediapicker v-model="item.videoId" />
</ui-property>
<ui-property label="@videopicker.fields.title">
<input type="text" v-model="item.title" maxlength="60" />
</ui-property>
<ui-property v-if="item.provider == 'html'" label="@videopicker.fields.previewImageId">
<ui-mediapicker v-model="item.videoId" />
</ui-property>
</div>
<div class="ui-box" v-if="preview">
<div>
<img :src="preview.image" />
<p>
<strong>{{preview.title}}</strong><br />
{{preview.description}}
</p>
</div>
</div>
</div>
</ui-trinity>
</template>
<script>
import { getVideoId, getVimeoMetadata, getYoutubeMetadata } from '../videoparser';
import { debounce } from '../../../utils';
const PROVIDERS = [
{ label: '@videopicker.providers.html', value: 'html' },
{ label: '@videopicker.providers.youtube', value: 'youtube' },
{ label: '@videopicker.providers.vimeo', value: 'vimeo' },
];
export default {
props: {
model: Object,
config: Object
},
data: () => ({
opened: false,
state: 'default',
providers: PROVIDERS,
item: null,
videoIdParsing: false,
template: {
provider: 'youtube',
videoId: null,
videoUrl: null,
videoPreviewImageUrl: null,
title: null,
previewImageId: null
},
preview: null,
debouncedReloadPreview: null
}),
watch: {
'item.provider': function (val)
{
if (this.opened)
{
this.item.videoId = null;
this.item.videoUrl = null;
this.item.videoPreviewImageUrl = null;
this.item.title = null;
this.item.previewImageId = null;
this.preview = null;
}
}
},
mounted()
{
this.debouncedReloadPreview = debounce(this.reloadPreview, 300);
this.item = JSON.parse(JSON.stringify(this.model || this.template));
setTimeout(() =>
{
this.opened = true;
if (this.model && this.model.provider !== 'html' && this.model.videoId)
{
this.reloadPreview(this.model.videoId);
}
}, 300);
},
methods: {
onUrlChange(url)
{
this.parseUrl(url);
},
parseUrl(url)
{
this.item.videoId = getVideoId(url);
this.debouncedReloadPreview(this.item.videoId);
},
reloadPreview(id)
{
if (!id)
{
this.preview = null;
return;
}
if (this.item.provider === 'vimeo')
{
getVimeoMetadata(id).then(res =>
{
this.preview = res.success ? res : null;
this.handlePreview(this.preview);
});
}
else
{
getYoutubeMetadata(id).then(res =>
{
this.preview = res.success ? res : null;
this.handlePreview(this.preview);
});
}
},
handlePreview(preview)
{
if (!preview)
{
return;
}
this.item.title = this.preview.title;
this.item.videoPreviewImageUrl = this.preview.image;
//this.item.videoUrl = this.preview.url;
},
onSave()
{
this.config.confirm(this.item);
}
}
}
</script>
<style lang="scss">
.ui-videopicker-overlay content
{
padding-top: 0;
}
.ui-videopicker-overlay-options .ui-property
{
display: flex;
justify-content: space-between;
}
.ui-videopicker-overlay-options .ui-property + .ui-property
{
margin-top: var(--padding-m);
}
.ui-videopicker-overlay-options .ui-property-content
{
display: inline;
flex: 0 0 auto;
}
.ui-videopicker-overlay-options .ui-property-label
{
padding-top: 1px;
}
</style>
@@ -7,9 +7,11 @@ export default {
install(app: ZeroPluginOptions)
{
app.vue.component('ui-mediapicker', defineAsyncComponent(() => import('./components/ui-mediapicker.vue')));
app.vue.component('ui-videopicker', defineAsyncComponent(() => import('./components/ui-videopicker.vue')));
app.fieldType('media', defineAsyncComponent(() => import('./components/field-mediapicker.vue')));
app.fieldType('image', defineAsyncComponent(() => import('./components/field-imagepicker.vue')));
app.fieldType('video', defineAsyncComponent(() => import('./components/field-videopicker.vue')));
app.route({ name: 'media', path: '/media/:parentId?', component: () => import('./pages/overview/overview.vue'), props: true });
app.route({ name: 'media-edit', path: '/media/edit/:id?', component: () => import('./pages/detail/detail.vue'), props: true });
@@ -35,6 +37,12 @@ declare module 'zero/schemas'
* @param {MediaBaseFieldOptions} [options] - Custom options
*/
image(options?: MediaBaseFieldOptions): ZeroEditorField;
/**
* Renders an video picker
* @param {PickerFieldOptions} [options] - Custom options
*/
video(options?: PickerFieldOptions): ZeroEditorField;
}
export type MediaTypeFor = 'all' | 'images' | 'videos' | 'documents';
@@ -0,0 +1,110 @@
export const YOUTUBE_REGEX = /youtu(?:\.be|be\.com)\/(?:.*v(?:\/|=)|(?:.*\/)?)([a-zA-Z0-9-_]+)/gim;
export const VIMEO_REGEX = /vimeo\.com\/(\d+)($|\/)/gim;
/**
* Get video ID from an URL.
* This currently works for YouTube and Vimeo links.
* @param {string} url
*/
export let getVideoId = url =>
{
if (!url)
{
return null;
}
if (url.indexOf('vimeo.com') > -1)
{
let matches = VIMEO_REGEX.exec(url);
return matches && matches[1];
}
else if (url.indexOf('youtu') > -1)
{
let matches = YOUTUBE_REGEX.exec(url);
return matches && matches[1];
}
return null;
};
/**
* Get metadata for a vimeo ID.
* @param {string} id
*/
export let getVimeoMetadata = async id =>
{
let result = {
id,
url: `https://vimeo.com/${id}`,
success: false
};
let response = await fetch(`https://vimeo.com/api/v2/video/${id}.json`);
if (response.ok)
{
let data = await response.json();
result.data = data[0];
result.image = result.data.thumbnail_large;
result.title = result.data.title;
result.description = result.data.description;
result.success = true;
}
return result;
};
/**
* Get metdata for a YouTube ID.
* @param {string} url
*/
export let getYoutubeMetadata = async id =>
{
const apiKey = 'AIzaSyD720TVsYEil4PTESJ9jD0Xijd0zldExmc'; // TODO v3 make configurable
let result = {
id,
image: `https://i3.ytimg.com/vi/${id}/maxresdefault.jpg`,
url: `https://www.youtube.com/watch?v=${id}`,
success: true
};
if (apiKey)
{
result.success = false;
let response = await fetch(`https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${id}&key=${apiKey}`);
if (response.ok)
{
let data = await response.json();
if (data && data.items && data.items.length)
{
result.data = data.items[0].snippet;
let thumbs = Object.values(result.data.thumbnails);
let thumb = thumbs[thumbs.length - 1];
result.image = thumb.url;
result.title = result.data.title;
result.description = result.data.description;
result.success = true;
}
}
return result;
}
else
{
return {
id,
image: `https://i3.ytimg.com/vi/${id}/maxresdefault.jpg`,
url: `https://www.youtube.com/watch?v=${id}`,
success: true
};
}
};
@@ -4,6 +4,9 @@ import { formatDate } from '../../utils/dates';
const editor = new ZeroEditor('demo');
editor.field('categoryIds').categoryPicker({ limit: 20, pickChannel: true });
editor.field('video').video();
editor.field('rte', { label: 'Number' }).rte();
editor.field('rte').output({ html: false });
editor.field('link').linkPicker();
@@ -26,7 +26,9 @@
productId1: null,
productId2: [],
productId3: null,
link: null
link: null,
categoryIds: [],
video: null
},
route: 'demo',
disabled: false
@@ -6,7 +6,7 @@ export default {
install(app: ZeroPluginOptions)
{
app.route({ name: 'settings', path: '/settings', component: () => import('./settings.vue') });
//app.route({ name: 'demo', path: '/settings/demo', component: () => import('./demo.vue') });
app.route({ name: 'demo', path: '/settings/demo', component: () => import('./demo.vue') });
app.schema('demo', () => import('./demo'));
}
@@ -45,17 +45,17 @@
this.groups = useUiStore().settingGroups;
this.appCount = useAppStore().applications.length;
//if (!this.groups[1].items.find(x => x.alias === 'demo'))
//{
// this.groups[1].items.push({
// alias: "demo",
// description: "Demo all editor components",
// icon: "fth-box",
// isPlugin: true,
// name: "Components",
// url: "/settings/demo"
// });
//}
if (!this.groups[1].items.find(x => x.alias === 'demo'))
{
this.groups[1].items.push({
alias: "demo",
description: "Demo all editor components",
icon: "fth-box",
isPlugin: true,
name: "Components",
url: "/settings/demo"
});
}
}
}
</script>
@@ -658,6 +658,14 @@
"html": "Media upload",
"youtube": "YouTube",
"vimeo": "Vimeo"
},
"fields": {
"provider": "Provider",
"videoUrl": "URL",
"videoId": "Pick video",
"title": "Title",
"previewImageId": "Poster image",
"foundParsed": "Parsed video ID is <b>{id}</b>"
}
},
@@ -80,9 +80,6 @@ public class ResourceService : IResourceService
catch { }
}
Logger.LogInformation("translation path [0]: " + Path.Combine(AppContext.BaseDirectory, path));
Logger.LogInformation("translation path [1]: " + fullpath);
if (!File.Exists(fullpath))
{
return items;