video picker
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user