feat: redesigned video block

This commit is contained in:
Jannat Patel
2025-04-23 17:06:29 +05:30
parent 93b5cb6161
commit 6b412106de
9 changed files with 2934 additions and 5565 deletions

View File

@@ -71,6 +71,7 @@ declare module 'vue' {
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
PaymentSettings: typeof import('./src/components/PaymentSettings.vue')['default']
Play: typeof import('./src/components/Icons/Play.vue')['default']
ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
Question: typeof import('./src/components/Modals/Question.vue')['default']
Quiz: typeof import('./src/components/Quiz.vue')['default']

View File

@@ -31,12 +31,14 @@
"lucide-vue-next": "^0.383.0",
"markdown-it": "^14.0.0",
"pinia": "^2.0.33",
"plyr": "^3.7.8",
"socket.io-client": "^4.7.2",
"tailwindcss": "3.4.15",
"typescript": "^5.7.2",
"vue": "^3.4.23",
"vue-chartjs": "^5.3.0",
"vue-draggable-next": "^2.2.1",
"vue-plyr": "^7.0.0",
"vue-router": "^4.0.12",
"vue3-apexcharts": "^1.8.0",
"vuedraggable": "4.1.0"

View File

@@ -0,0 +1,16 @@
<template>
<svg
width="30"
height="30"
viewBox="0 0 68 75"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 6.78182C0 1.60212 5.5742 -1.65958 10.09 0.879521L64.09 31.2545C68.6916 33.8443 68.6916 40.4693 64.09 43.0595L10.09 73.4345C5.5744 75.9736 0 72.7119 0 67.5322V6.78182ZM26.2695 38.5201C26.2695 37.3248 25.2265 37.9342 26.2695 38.5201C27.332 39.1178 27.332 37.9225 26.2695 38.5201Z"
fill="white"
/>
</svg>
</template>

View File

@@ -1,32 +1,53 @@
<template>
<div ref="videoContainer" class="video-block group relative">
<div ref="videoContainer" class="video-block relative group">
<video
@timeupdate="updateTime"
@ended="videoEnded"
@click="togglePlay"
oncontextmenu="return false"
class="rounded-lg border border-gray-100 group cursor-pointer"
class="rounded-md border border-gray-100 cursor-pointer"
ref="videoRef"
>
<source :src="fileURL" :type="type" />
</video>
<div
class="flex items-center space-x-2 bg-surface-gray-3 rounded-md p-0.5 absolute bottom-3 w-[98%] left-0 right-0 mx-auto invisible group-hover:visible"
v-if="!playing"
class="absolute inset-0 flex items-center justify-center cursor-pointer"
@click="playVideo"
>
<div
class="rounded-full p-5 pl-6"
style="
background: radial-gradient(
circle,
rgba(0, 0, 0, 0.4) 0%,
rgba(0, 0, 0, 0.5) 50%
);
"
>
<Play />
</div>
</div>
<div
class="flex items-center space-x-2 py-2 px-1 text-ink-white bg-gradient-to-b from-transparent to-black/75 absolute bottom-0 left-0 right-0 mx-auto rounded-md"
:class="{
'invisible group-hover:visible': playing,
}"
>
<Button variant="ghost">
<template #icon>
<Play
v-if="!playing"
@click="playVideo"
class="w-4 h-4 text-ink-gray-9"
class="size-4 text-ink-gray-9"
/>
<Pause v-else @click="pauseVideo" class="w-4 h-4 text-ink-gray-9" />
<Pause v-else @click="pauseVideo" class="size-5 text-ink-white" />
</template>
</Button>
<Button variant="ghost" @click="toggleMute">
<template #icon>
<Volume2 v-if="!muted" class="w-4 h-4 text-ink-gray-9" />
<VolumeX v-else class="w-4 h-4 text-ink-gray-9" />
<Volume2 v-if="!muted" class="size-5 text-ink-white" />
<VolumeX v-else class="size-5 text-ink-white" />
</template>
</Button>
<input
@@ -38,12 +59,12 @@
@input="changeCurrentTime"
class="duration-slider w-full h-1"
/>
<span class="text-xs font-medium">
<span class="text-sm font-semibold">
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
</span>
<Button variant="ghost" @click="toggleFullscreen">
<template #icon>
<Maximize class="w-4 h-4 text-ink-gray-9" />
<Maximize class="size-5 text-ink-white" />
</template>
</Button>
</div>
@@ -51,8 +72,9 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { Play, Pause, Maximize, Volume2, VolumeX } from 'lucide-vue-next'
import { Pause, Maximize, Volume2, VolumeX } from 'lucide-vue-next'
import { Button } from 'frappe-ui'
import Play from '@/components/Icons/Play.vue'
const videoRef = ref(null)
const videoContainer = ref(null)
@@ -165,15 +187,16 @@ iframe {
flex: 1;
-webkit-appearance: none;
appearance: none;
background-color: theme('colors.gray.400');
border-radius: 10px;
background-color: theme('colors.gray.100');
cursor: pointer;
}
.duration-slider::-webkit-slider-thumb {
height: 10px;
width: 10px;
width: 2px;
border-radius: 50%;
-webkit-appearance: none;
background-color: theme('colors.gray.900');
background-color: theme('colors.gray.500');
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
@@ -186,7 +209,7 @@ iframe {
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
cursor: pointer;
box-shadow: -500px 0 0 500px theme('colors.gray.900');
box-shadow: -500px 0 0 500px theme('colors.gray.600');
}
}
</style>

View File

@@ -245,6 +245,8 @@ import LessonContent from '@/components/LessonContent.vue'
import CourseInstructors from '@/components/CourseInstructors.vue'
import ProgressBar from '@/components/ProgressBar.vue'
import CertificationLinks from '@/components/CertificationLinks.vue'
import Plyr from 'plyr'
import 'plyr/dist/plyr.css'
const user = inject('$user')
const router = useRouter()
@@ -276,6 +278,7 @@ const props = defineProps({
onMounted(() => {
startTimer()
enablePlyr()
document.addEventListener('fullscreenchange', attachFullscreenEvent)
})
@@ -291,6 +294,34 @@ onBeforeUnmount(() => {
document.removeEventListener('fullscreenchange', attachFullscreenEvent)
})
const enablePlyr = () => {
setTimeout(() => {
let src = document
.getElementsByClassName('video-player')[0]
.getAttribute('src')
if (src) {
let videoID = src.split('/').pop()
document
.getElementsByClassName('video-player')[0]
.setAttribute('data-plyr-embed-id', videoID)
}
new Plyr('.video-player', {
youtube: {
noCookie: true,
},
controls: [
'play-large',
'play',
'progress',
'current-time',
'mute',
'volume',
'fullscreen',
],
})
}, 500)
}
const lesson = createResource({
url: 'lms.lms.utils.get_lesson',
cache: ['lesson', props.courseName, props.chapterNumber, props.lessonNumber],
@@ -401,6 +432,7 @@ watch(
clearInterval(timerInterval)
timer.value = 0
startTimer()
enablePlyr()
}
}
)
@@ -655,4 +687,7 @@ usePageMeta(() => {
.tc-table {
border-left: 1px solid #e8e8eb;
}
.plyr__control--overlaid {
}
</style>

View File

@@ -199,55 +199,9 @@ export function getEditorTools() {
services: {
youtube: {
regex: /(?:https?:\/\/)?(?:www\.)?(?:(?:youtu\.be\/)|(?:youtube\.com)\/(?:v\/|u\/\w\/|embed\/|watch))(?:(?:\?v=)?([^#&?=]*))?((?:[?&]\w*=\w*)*)/,
embedUrl:
'https://www.youtube.com/embed/<%= remote_id %>?modestbranding=1&enablejsapi=1&widgetid=3&iv_load_policy=3&fs=0',
html: `<iframe style="width:100%; height: ${
window.innerWidth < 640 ? '15rem' : '30rem'
};" frameborder="0" allowfullscreen></iframe>`,
id: ([id, params]) => {
if (!params && id) {
return id
}
const paramsMap = {
start: 'start',
end: 'end',
t: 'start',
// eslint-disable-next-line camelcase
time_continue: 'start',
list: 'list',
}
let newParams = params
.slice(1)
.split('&')
.map((param) => {
const [name, value] = param.split('=')
if (!id && name === 'v') {
id = value
return null
}
if (!paramsMap[name]) {
return null
}
if (
value === 'LL' ||
value.startsWith('RDMM') ||
value.startsWith('FL')
) {
return null
}
return `${paramsMap[name]}=${value}`
})
.filter((param) => !!param)
return id + '?' + newParams.join('&')
},
embedUrl: '<%= remote_id %>',
/* 'https://www.youtube.com/embed/<%= remote_id %>?origin=https://plyr.io&amp;iv_load_policy=3&amp;modestbranding=1&amp;playsinline=1&amp;showinfo=0&amp;rel=0&amp;enablejsapi=1' */ html: `<div class="video-player" data-plyr-provider="youtube"></div>`,
id: ([id]) => id,
},
vimeo: {
regex: /(?:http[s]?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/,

View File

@@ -40,6 +40,7 @@ export default defineConfig({
'engine.io-client',
'tailwind.config.js',
'highlight.js',
'plyr',
],
},
})

2838
frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

5501
yarn.lock

File diff suppressed because it is too large Load Diff