Skip to content
Merged
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
17 changes: 17 additions & 0 deletions src/frontend/fields/attachments/bunny-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,23 @@ export default class BunnyService implements HostService {
return { url: '' };
};

public async fetchVideoTitle(videoId: string): Promise<{ title?: string }> {
const { default: axios } = await import('axios');

const response = await axios.get(
`${authority}/library/${this.libraryId}/videos/${videoId}`,
{
headers: {
AccessKey: this.accessKey,
},
},
);

return {
title: response.data.title,
};
}

public get url(): string | null {
return this.guid ? `https://${this.host}/${this.guid}/playlist.m3u8` : null;
}
Expand Down
17 changes: 17 additions & 0 deletions src/frontend/fields/attachments/video-meta.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<Story id="video-meta" title="Video Meta" group="widgets">
<Variant title="">
<VideoMeta :metadata-rows="metadataRows" />
</Variant>
</Story>
</template>

<script setup lang="ts">
import VideoMeta from './video-meta.vue';

const metadataRows = [
{ label: 'Title', value: 'My Video' },
{ label: 'Video ID', value: '1234567890' },
{ label: 'URL', value: 'https://www.youtube.com/watch?v=1234567890' },
];
</script>
43 changes: 43 additions & 0 deletions src/frontend/fields/attachments/video-meta.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<div class="flex flex-col gap-[19px]">
<div v-for="row in metadataRows" :key="row.label">
<label class="block text-sm font-semibold text-gray-500">
{{ row.label }}
</label>
<div
class="mt-2 flex items-center rounded-md border border-gray-200 bg-gray-50 px-3 py-2"
>
<span class="min-w-0 flex-1 truncate text-sm text-gray-900" :title="row.value">
{{ row.value }}
</span>
<button
type="button"
class="shrink-0 rounded p-1 text-gray-500 transition-colors hover:bg-gray-200 hover:text-gray-700"
:title="copiedLabel === row.label ? 'Copied!' : `Copy ${row.label}`"
@click="copyToClipboard(row.value, row.label)"
>
<Icon :name="copiedLabel === row.label ? 'check' : 'clipboard'" />
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import Icon from '../../shared/icon.vue';

defineProps<{
metadataRows: { label: string; value: string }[];
}>();

const copiedLabel = ref<string | null>(null);
const RESET_DELAY = 1500;

const copyToClipboard = (value: string, label: string) => {
navigator.clipboard.writeText(value);
copiedLabel.value = label;
setTimeout(() => {
copiedLabel.value = null;
}, RESET_DELAY);
};
</script>
10 changes: 8 additions & 2 deletions src/frontend/fields/attachments/video-player.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<template>
<div class="w-auto h-48">
<iframe :src="`${source}?autoplay=false&loop=false&muted=false&preload=false&responsive=true`" style="height: 100%; width: 100%" loading="lazy" allow="accelerometer;gyroscope;autoplay;encrypted-media;picture-in-picture;" allowfullscreen="true">
<div class="h-full w-auto">
<iframe
:src="`${source}?autoplay=false&loop=false&muted=false&preload=false&responsive=true`"
style="height: 100%; width: 100%"
loading="lazy"
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture"
allowfullscreen="true"
>
</iframe>
</div>
</template>
Expand Down
42 changes: 40 additions & 2 deletions src/frontend/fields/video-field.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
@attached="onAttached"
@dropped="onDropped"
>
<VideoPlayer :url="previewUrl" :library="host.library" />
<div class="grid w-full gap-8" :class="isStacked ? 'grid-cols-1' : 'grid-cols-2'">
<div class="overflow-hidden rounded-md">
<VideoPlayer :url="previewUrl" :library="host.library" />
</div>
<VideoMeta :metadata-rows="metadataRows" />
</div>
</AttachmentField>

<div v-if="feedback" class="text-sm text-red-300">
Expand All @@ -27,7 +32,7 @@
</template>

<script setup lang="ts">
import { computed, nextTick, ref } from 'vue';
import { computed, nextTick, ref, onMounted } from 'vue';
import type { FieldSpec, Video } from '../../types';
import { useModelStore, useSharedStore } from '../store';
import { commonProps } from '../shared/helpers';
Expand All @@ -36,6 +41,7 @@ import BunnyService from './attachments/bunny-service';
import type { AttachmentModel } from './attachments/types';
import VideoPlayer from './attachments/video-player.vue';
import VideoProgress from './attachments/video-progress.vue';
import VideoMeta from './attachments/video-meta.vue';

const props = defineProps({
...commonProps,
Expand Down Expand Up @@ -75,6 +81,8 @@ const onSuccess = (url: string) => {
});
};

const isStacked = computed(() => props.isNested === true);

const onError = (error: Error) => {
console.log('error', error);
feedback.value = error.message ?? 'Something went wrong';
Expand Down Expand Up @@ -122,4 +130,34 @@ const onAttached = (_data: AttachmentModel) => {
// for video, the file is not uploaded yet,
// so we need to wait for the onSuccess callback
};

const extractVideoIdFromUrl = (videoUrl: string | null): string | null => {
if (!videoUrl) return null;
const pieces = videoUrl.split('/');
return pieces.length >= 4 ? (pieces[pieces.length - 2] ?? null) : null;
};

const videoId = computed(() => extractVideoIdFromUrl(url.value));

const displayUrl = computed(() => {
if (videoId.value && host.library) {
return `https://player.mediadelivery.net/embed/${host.library}/${videoId.value}`;
}
return url.value ?? '—';
});

const videoTitle = ref<string | null>(null);

const metadataRows = computed(() => [
{ label: 'Title', value: videoTitle.value ?? '—' },
{ label: 'Video ID', value: videoId.value ?? '—' },
{ label: 'URL', value: displayUrl.value },
]);

onMounted(async () => {
if (videoId.value && host.library) {
const { title } = await host.fetchVideoTitle(videoId.value);
videoTitle.value = title ?? null;
}
});
</script>
14 changes: 14 additions & 0 deletions src/frontend/shared/icon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,20 @@
fill="#1D4ED8"
/>
</svg>
<svg
v-if="name == 'clipboard'"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 19V9C3 7.34315 4.34315 6 6 6H7V5C7 3.34315 8.34315 2 10 2H14.5859C15.1163 2.00004 15.625 2.2109 16 2.58594L20.4141 7C20.7891 7.37504 21 7.88369 21 8.41406V15C21 16.6569 19.6569 18 18 18H17V19C17 20.6569 15.6569 22 14 22H6C4.34315 22 3 20.6569 3 19ZM5 19C5 19.5523 5.44772 20 6 20H14C14.5523 20 15 19.5523 15 19V18H10C8.34315 18 7 16.6569 7 15V8H6C5.44772 8 5 8.44772 5 9V19ZM9 15C9 15.5523 9.44772 16 10 16H18C18.5523 16 19 15.5523 19 15V8.41406L14.5859 4H10C9.44772 4 9 4.44772 9 5V15Z"
fill="#111827"
/>
</svg>

<svg
v-if="name == 'user-add'"
width="16"
Expand Down