Browse Source

feat: native video player

pull/8990/head
DarkPhoenix2704 4 months ago
parent
commit
2aa05ebe09
No known key found for this signature in database
GPG Key ID: 3F76B10622A07849
  1. 5
      packages/nc-gui/assets/nc-icons/play.svg
  2. 90
      packages/nc-gui/components/cell/attachment/Carousel.vue
  3. 6
      packages/nc-gui/components/cell/attachment/Modal.vue
  4. 23
      packages/nc-gui/components/cell/attachment/Video.vue
  5. 12
      packages/nc-gui/components/cell/attachment/index.vue
  6. 4
      packages/nc-gui/components/cell/attachment/utils.ts
  7. 5
      packages/nc-gui/components/nc/Carousel/Item.vue
  8. 27
      packages/nc-gui/utils/fileUtils.ts
  9. 2
      packages/nc-gui/utils/iconUtils.ts

5
packages/nc-gui/assets/nc-icons/play.svg

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="play">
<path id="Vector" d="M3.33331 2L12.6666 8L3.33331 14V2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 273 B

90
packages/nc-gui/components/cell/attachment/Carousel.vue

@ -1,52 +1,71 @@
<script lang="ts" setup>
import type { CarouselApi } from '../../nc/Carousel/interface'
import { useAttachmentCell } from './utils'
const { selectedImage, visibleItems, downloadAttachment } = useAttachmentCell()!
const container = ref()
const container = ref<HTMLElement | null>(null)
const emblaMainApi: CarouselApi = ref()
const emblaThumbnailApi: CarouselApi = ref()
const selectedIndex = ref(0)
const selectedIndex = ref()
const { getPossibleAttachmentSrc } = useAttachment()
useEventListener(container, 'click', (e) => {
if (!(e.target as HTMLElement)?.closest('.keep-open') && !(e.target as HTMLElement)?.closest('img')) {
selectedImage.value = false
const target = e.target as HTMLElement
if (!target.closest('.keep-open') && !target.closest('img')) {
selectedFile.value = false
}
})
function onThumbClick(index: number) {
const onThumbClick = (index: number) => {
if (!emblaMainApi.value || !emblaThumbnailApi.value) return
emblaMainApi.value.scrollTo(index)
emblaThumbnailApi.value.scrollTo(index)
}
function onSelect() {
const onSelect = () => {
if (!emblaMainApi.value || !emblaThumbnailApi.value) return
selectedIndex.value = emblaMainApi.value?.selectedScrollSnap()
selectedImage.value = visibleItems.value[emblaMainApi.value?.selectedScrollSnap()]
emblaThumbnailApi.value.scrollTo(emblaMainApi.value.selectedScrollSnap())
const newSnap = emblaMainApi.value.selectedScrollSnap()
selectedIndex.value = newSnap
selectedFile.value = visibleItems.value[newSnap]
emblaThumbnailApi.value.scrollTo(newSnap)
}
watchOnce(emblaMainApi, (emblaMainApi) => {
watchOnce(emblaMainApi, async (emblaMainApi) => {
if (!emblaMainApi) return
onSelect()
emblaMainApi.on('select', onSelect)
emblaThumbnailApi.value?.on('reInit', onSelect)
emblaMainApi.on('select', onSelect)
await nextTick(() => {
if (!selectedIndex.value) {
const newIndex = visibleItems.value.findIndex((item) => {
if (selectedFile.value?.path) return item?.path === selectedFile.value.path
if (selectedFile.value?.url) return item?.url === selectedFile.value.url
return selectedFile.value?.title === item?.title
})
selectedIndex.value = newIndex
emblaMainApi.scrollTo(newIndex)
}
})
})
</script>
<template>
<GeneralOverlay v-model="selectedImage" transition :z-index="1001" class="bg-black bg-opacity-90">
<div v-if="selectedImage" class="flex">
<div ref="container" class="overflow-hidden text-center relative h-screen">
<GeneralOverlay v-model="selectedFile" transition :z-index="1001" class="bg-black bg-opacity-90">
<div v-if="selectedFile" class="flex w-full justify-center items-center">
<div ref="container" class="overflow-hidden w-full flex items-center justify-center text-center relative h-screen">
<NcButton
class="top-5 !absolute cursor-pointer !hover:bg-transparent left-5"
size="xsmall"
type="text"
@click.stop="selectedImage = false"
@click.stop="selectedFile = false"
>
<component :is="iconMap.close" class="text-white" />
</NcButton>
@ -59,7 +78,7 @@ watchOnce(emblaMainApi, (emblaMainApi) => {
class="hover:underline pointer-events-auto font-semibold cursor-pointer text-white"
@click.stop="downloadAttachment(selectedImage)"
>
{{ selectedImage && selectedImage.title }}
{{ selectedFile && selectedFile.title }}
</h3>
</div>
@ -67,35 +86,50 @@ watchOnce(emblaMainApi, (emblaMainApi) => {
<NcCarouselContent>
<NcCarouselItem v-for="(item, index) in visibleItems" :key="index">
<LazyCellAttachmentImage
class="nc-attachment-img-wrapper !h-[80%]"
v-if="isImage(item.title, item.mimeType)"
class="nc-attachment-img-wrapper !h-[60%]"
object-fit="contain"
:alt="item.title"
:srcs="getPossibleAttachmentSrc(item)"
/>
<LazyCellAttachmentVideo
v-else-if="isVideo(item.title, item.mimeType)"
class="!h-[60%]"
:alt="item.title"
:srcs="getPossibleAttachmentSrc(item)"
/>
</NcCarouselItem>
</NcCarouselContent>
<NcCarouselPrevious size="small" class="!top-5/12 z-20 !left-8 !absolute" />
<NcCarouselNext size="small" class="!top-5/12 z-20 !right-8 !absolute" />
</NcCarousel>
<div class="absolute !w-screen !bottom-5 max-h-18 z-30 flex items-center justify-center inset-x-0">
<div class="absolute w-full !bottom-5 max-h-18 z-30 flex items-center justify-center">
<NcCarousel class="absolute max-w-sm" @init-api="(val) => (emblaThumbnailApi = val)">
<NcCarouselContent class="!flex !gap-3 ml-0">
<NcCarouselContent class="!flex !gap-2 ml-0">
<NcCarouselItem
v-for="(item, index) in visibleItems"
:key="index"
:class="{
'nc-active-attachment': index === selectedIndex,
' opacity-100': index === selectedIndex,
}"
class="pl-0 opacity-50 !basis-1/4 cursor-pointer"
class="px-2 keep-open opacity-50 !basis-1/8 cursor-pointer"
@click="onThumbClick(index)"
>
<div class="flex items-center justify-center">
<LazyCellAttachmentImage
class="nc-attachment-img-wrapper"
v-if="isImage(item.title, item.mimeType)"
class="nc-attachment-img-wrapper h-12"
object-fit="contain"
:alt="item.title"
:srcs="getPossibleAttachmentSrc(item)"
/>
<div
v-else-if="isVideo(item.title, item.mimeType)"
class="h-full flex items-center h-6 justify-center rounded-md px-2 py-1 border-1 border-gray-200"
>
<GeneralIcon class="text-white" icon="play" />
</div>
</div>
</NcCarouselItem>
</NcCarouselContent>
</NcCarousel>
@ -109,8 +143,4 @@ watchOnce(emblaMainApi, (emblaMainApi) => {
.nc-attachment-img-wrapper {
width: fit-content !important;
}
.nc-active-attachment {
@apply transform opacity-100 scale-110 transition-transform;
}
</style>

6
packages/nc-gui/components/cell/attachment/Modal.vue

@ -18,7 +18,7 @@ const {
onDrop,
downloadAttachment,
updateModelValue,
selectedImage,
selectedFile,
selectedVisibleItems,
bulkDownloadAttachments,
renameFile,
@ -42,10 +42,10 @@ onKeyDown('Escape', () => {
})
function onClick(item: Record<string, any>) {
selectedImage.value = item
selectedFile.value = item
modalVisible.value = false
const stopHandle = watch(selectedImage, (nextImage) => {
const stopHandle = watch(selectedFile, (nextImage) => {
if (!nextImage) {
setTimeout(() => {
modalVisible.value = true

23
packages/nc-gui/components/cell/attachment/Video.vue

@ -0,0 +1,23 @@
<script setup lang="ts">
interface Props {
srcs: string[]
alt?: string
mimeType?: string
objectFit?: string
}
const props = defineProps<Props>()
const index = ref(0)
const onError = () => index.value++
</script>
<template>
<video controls>
<source :src="props.srcs[index]" :type="props.mimeType" />
Your browser does not support the video tag.
</video>
</template>
<style scoped lang="scss"></style>

12
packages/nc-gui/components/cell/attachment/index.vue

@ -51,7 +51,7 @@ const {
onDrop,
isLoading,
FileIcon,
selectedImage,
selectedFile,
isReadonly,
storedFiles,
removeFile,
@ -163,12 +163,12 @@ const onExpand = () => {
modalVisible.value = true
}
const onImageClick = (item: any) => {
const onFileClick = (item: any) => {
if (isMobileMode.value && !isExpandedForm.value) return
if (!isMobileMode.value && (isGallery.value || isKanban.value) && !isExpandedForm.value) return
selectedImage.value = item
selectedFile.value = item
}
const keydownEnter = (e: KeyboardEvent) => {
@ -219,7 +219,7 @@ const handleFileDelete = (i: number) => {
class="nc-attachment-cell relative flex color-transition gap-2 flex items-center w-full xs:(min-h-12 max-h-32)"
:class="{ 'justify-center': !active, 'justify-between': active, 'px-2': isExpandedForm }"
>
<LazyCellAttachmentCarousel />
<LazyCellAttachmentCarousel v-if="selectedFile" />
<template v-if="isSharedForm || (!isReadonly && !dragging && !!currentCellRef)">
<general-overlay
@ -291,7 +291,7 @@ const handleFileDelete = (i: number) => {
<div
class="nc-attachment flex items-center flex-col flex-wrap justify-center flex-auto"
:class="{ '!w-30': isForm || isExpandedForm }"
@click="() => onImageClick(item)"
@click="() => onFileClick(item)"
>
<LazyCellAttachmentImage
:alt="item.title || `#${i}`"
@ -318,7 +318,7 @@ const handleFileDelete = (i: number) => {
'h-20.8 !w-30': rowHeight === 6 || isForm || isExpandedForm,
'ml-2': active,
}"
@click="openAttachment(item)"
@click="onFileClick(item)"
>
<component :is="FileIcon(item.icon)" v-if="item.icon" :class="{ 'h-13 w-13': isForm || isExpandedForm }" />

4
packages/nc-gui/components/cell/attachment/utils.ts

@ -42,7 +42,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const modalVisible = ref(false)
/** for image carousel */
const selectedImage = ref()
const selectedFile = ref()
const videoStream = ref<MediaStream | null>(null)
@ -414,7 +414,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
renameFile,
downloadAttachment,
updateModelValue,
selectedImage,
selectedFile,
uploadViaUrl,
selectedVisibleItems,
storedFiles,

5
packages/nc-gui/components/nc/Carousel/Item.vue

@ -1,10 +1,7 @@
<script setup lang="ts">
import { useCarousel } from './useCarousel'
import type { WithClassAsProps } from './interface'
const props = defineProps<WithClassAsProps>()
const { orientation } = useCarousel()
</script>
<template>
@ -14,7 +11,7 @@ const { orientation } = useCarousel()
:class="{
[props.class]: props.class,
}"
class="min-w-0 shrink-0 embla__slide grow-0 basis-full"
class="min-w-0 shrink-0 flex justify-center items-center embla__slide grow-0 basis-full"
>
<slot />
</div>

27
packages/nc-gui/utils/fileUtils.ts

@ -15,11 +15,36 @@ const imageExt = [
'heic-sequence',
]
const videoExt = [
'webm',
'mpg',
'mp2',
'mpeg',
'ogg',
'mp4',
'm4v',
'avi',
'wmv',
'mov',
'qt',
'flv',
'mkv',
'3gp',
'3g2',
'vob',
'ts',
'ts',
]
const isVideo = (name: string, mimetype?: string) => {
return videoExt.some((e) => name?.toLowerCase().endsWith(`.${e}`)) || mimetype?.startsWith('video/')
}
const isImage = (name: string, mimetype?: string) => {
return imageExt.some((e) => name?.toLowerCase().endsWith(`.${e}`)) || mimetype?.startsWith('image/')
}
export { isImage, imageExt }
export { isImage, imageExt, isVideo }
// Ref : https://stackoverflow.com/a/12002275
// Tested in Mozilla Firefox browser, Chrome

2
packages/nc-gui/utils/iconUtils.ts

@ -204,6 +204,7 @@ import NcMaximize from '~icons/nc-icons/maximize'
import NcMaximizeAll from '~icons/nc-icons/maximize-all'
import NcDrag from '~icons/nc-icons/drag'
import NcRefresh from '~icons/nc-icons/refresh'
import NcPlay from '~icons/nc-icons/play'
// keep it for reference
// todo: remove it after all icons are migrated
@ -638,6 +639,7 @@ export const iconMap = {
ncDrag: NcDrag,
refresh: NcRefresh,
chevronUpDown: NcChevronUpDown,
play: NcPlay,
}
export const getMdiIcon = (type: string): any => {

Loading…
Cancel
Save