feat: redesigned video block
This commit is contained in:
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@@ -71,6 +71,7 @@ declare module 'vue' {
|
|||||||
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
|
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
|
||||||
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
|
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
|
||||||
PaymentSettings: typeof import('./src/components/PaymentSettings.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']
|
ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
|
||||||
Question: typeof import('./src/components/Modals/Question.vue')['default']
|
Question: typeof import('./src/components/Modals/Question.vue')['default']
|
||||||
Quiz: typeof import('./src/components/Quiz.vue')['default']
|
Quiz: typeof import('./src/components/Quiz.vue')['default']
|
||||||
|
|||||||
@@ -31,12 +31,14 @@
|
|||||||
"lucide-vue-next": "^0.383.0",
|
"lucide-vue-next": "^0.383.0",
|
||||||
"markdown-it": "^14.0.0",
|
"markdown-it": "^14.0.0",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
|
"plyr": "^3.7.8",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
"tailwindcss": "3.4.15",
|
"tailwindcss": "3.4.15",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vue": "^3.4.23",
|
"vue": "^3.4.23",
|
||||||
"vue-chartjs": "^5.3.0",
|
"vue-chartjs": "^5.3.0",
|
||||||
"vue-draggable-next": "^2.2.1",
|
"vue-draggable-next": "^2.2.1",
|
||||||
|
"vue-plyr": "^7.0.0",
|
||||||
"vue-router": "^4.0.12",
|
"vue-router": "^4.0.12",
|
||||||
"vue3-apexcharts": "^1.8.0",
|
"vue3-apexcharts": "^1.8.0",
|
||||||
"vuedraggable": "4.1.0"
|
"vuedraggable": "4.1.0"
|
||||||
|
|||||||
16
frontend/src/components/Icons/Play.vue
Normal file
16
frontend/src/components/Icons/Play.vue
Normal 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>
|
||||||
@@ -1,32 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="videoContainer" class="video-block group relative">
|
<div ref="videoContainer" class="video-block relative group">
|
||||||
<video
|
<video
|
||||||
@timeupdate="updateTime"
|
@timeupdate="updateTime"
|
||||||
@ended="videoEnded"
|
@ended="videoEnded"
|
||||||
@click="togglePlay"
|
@click="togglePlay"
|
||||||
oncontextmenu="return false"
|
oncontextmenu="return false"
|
||||||
class="rounded-lg border border-gray-100 group cursor-pointer"
|
class="rounded-md border border-gray-100 cursor-pointer"
|
||||||
ref="videoRef"
|
ref="videoRef"
|
||||||
>
|
>
|
||||||
<source :src="fileURL" :type="type" />
|
<source :src="fileURL" :type="type" />
|
||||||
</video>
|
</video>
|
||||||
<div
|
<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">
|
<Button variant="ghost">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Play
|
<Play
|
||||||
v-if="!playing"
|
v-if="!playing"
|
||||||
@click="playVideo"
|
@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>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" @click="toggleMute">
|
<Button variant="ghost" @click="toggleMute">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Volume2 v-if="!muted" class="w-4 h-4 text-ink-gray-9" />
|
<Volume2 v-if="!muted" class="size-5 text-ink-white" />
|
||||||
<VolumeX v-else class="w-4 h-4 text-ink-gray-9" />
|
<VolumeX v-else class="size-5 text-ink-white" />
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
<input
|
<input
|
||||||
@@ -38,12 +59,12 @@
|
|||||||
@input="changeCurrentTime"
|
@input="changeCurrentTime"
|
||||||
class="duration-slider w-full h-1"
|
class="duration-slider w-full h-1"
|
||||||
/>
|
/>
|
||||||
<span class="text-xs font-medium">
|
<span class="text-sm font-semibold">
|
||||||
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||||
</span>
|
</span>
|
||||||
<Button variant="ghost" @click="toggleFullscreen">
|
<Button variant="ghost" @click="toggleFullscreen">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Maximize class="w-4 h-4 text-ink-gray-9" />
|
<Maximize class="size-5 text-ink-white" />
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,8 +72,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
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 { Button } from 'frappe-ui'
|
||||||
|
import Play from '@/components/Icons/Play.vue'
|
||||||
|
|
||||||
const videoRef = ref(null)
|
const videoRef = ref(null)
|
||||||
const videoContainer = ref(null)
|
const videoContainer = ref(null)
|
||||||
@@ -165,15 +187,16 @@ iframe {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-color: theme('colors.gray.400');
|
border-radius: 10px;
|
||||||
|
background-color: theme('colors.gray.100');
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-slider::-webkit-slider-thumb {
|
.duration-slider::-webkit-slider-thumb {
|
||||||
height: 10px;
|
width: 2px;
|
||||||
width: 10px;
|
border-radius: 50%;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
background-color: theme('colors.gray.900');
|
background-color: theme('colors.gray.500');
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||||
@@ -186,7 +209,7 @@ iframe {
|
|||||||
input[type='range']::-webkit-slider-thumb {
|
input[type='range']::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: -500px 0 0 500px theme('colors.gray.900');
|
box-shadow: -500px 0 0 500px theme('colors.gray.600');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -245,6 +245,8 @@ import LessonContent from '@/components/LessonContent.vue'
|
|||||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||||
import ProgressBar from '@/components/ProgressBar.vue'
|
import ProgressBar from '@/components/ProgressBar.vue'
|
||||||
import CertificationLinks from '@/components/CertificationLinks.vue'
|
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||||
|
import Plyr from 'plyr'
|
||||||
|
import 'plyr/dist/plyr.css'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -276,6 +278,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
startTimer()
|
startTimer()
|
||||||
|
enablePlyr()
|
||||||
document.addEventListener('fullscreenchange', attachFullscreenEvent)
|
document.addEventListener('fullscreenchange', attachFullscreenEvent)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -291,6 +294,34 @@ onBeforeUnmount(() => {
|
|||||||
document.removeEventListener('fullscreenchange', attachFullscreenEvent)
|
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({
|
const lesson = createResource({
|
||||||
url: 'lms.lms.utils.get_lesson',
|
url: 'lms.lms.utils.get_lesson',
|
||||||
cache: ['lesson', props.courseName, props.chapterNumber, props.lessonNumber],
|
cache: ['lesson', props.courseName, props.chapterNumber, props.lessonNumber],
|
||||||
@@ -401,6 +432,7 @@ watch(
|
|||||||
clearInterval(timerInterval)
|
clearInterval(timerInterval)
|
||||||
timer.value = 0
|
timer.value = 0
|
||||||
startTimer()
|
startTimer()
|
||||||
|
enablePlyr()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -655,4 +687,7 @@ usePageMeta(() => {
|
|||||||
.tc-table {
|
.tc-table {
|
||||||
border-left: 1px solid #e8e8eb;
|
border-left: 1px solid #e8e8eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plyr__control--overlaid {
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -199,55 +199,9 @@ export function getEditorTools() {
|
|||||||
services: {
|
services: {
|
||||||
youtube: {
|
youtube: {
|
||||||
regex: /(?:https?:\/\/)?(?:www\.)?(?:(?:youtu\.be\/)|(?:youtube\.com)\/(?:v\/|u\/\w\/|embed\/|watch))(?:(?:\?v=)?([^#&?=]*))?((?:[?&]\w*=\w*)*)/,
|
regex: /(?:https?:\/\/)?(?:www\.)?(?:(?:youtu\.be\/)|(?:youtube\.com)\/(?:v\/|u\/\w\/|embed\/|watch))(?:(?:\?v=)?([^#&?=]*))?((?:[?&]\w*=\w*)*)/,
|
||||||
embedUrl:
|
embedUrl: '<%= remote_id %>',
|
||||||
'https://www.youtube.com/embed/<%= remote_id %>?modestbranding=1&enablejsapi=1&widgetid=3&iv_load_policy=3&fs=0',
|
/* 'https://www.youtube.com/embed/<%= remote_id %>?origin=https://plyr.io&iv_load_policy=3&modestbranding=1&playsinline=1&showinfo=0&rel=0&enablejsapi=1' */ html: `<div class="video-player" data-plyr-provider="youtube"></div>`,
|
||||||
html: `<iframe style="width:100%; height: ${
|
id: ([id]) => id,
|
||||||
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('&')
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
vimeo: {
|
vimeo: {
|
||||||
regex: /(?:http[s]?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/,
|
regex: /(?:http[s]?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export default defineConfig({
|
|||||||
'engine.io-client',
|
'engine.io-client',
|
||||||
'tailwind.config.js',
|
'tailwind.config.js',
|
||||||
'highlight.js',
|
'highlight.js',
|
||||||
|
'plyr',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
2838
frontend/yarn.lock
Normal file
2838
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user