Merge pull request #1461 from frappe/zen-mode

feat: Zen Mode
This commit is contained in:
Jannat Patel
2025-04-25 18:21:31 +05:30
committed by GitHub
16 changed files with 3335 additions and 5750 deletions

View File

@@ -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']

View File

@@ -31,6 +31,7 @@
"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",

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="h-full"> <div class="">
<div <div
v-if="title && (outline.data?.length || allowEdit)" v-if="title && (outline.data?.length || allowEdit)"
class="flex items-center justify-between space-x-2 mb-4 px-2" class="flex items-center justify-between space-x-2 mb-4 px-2"
@@ -17,9 +17,6 @@
<Button size="sm" v-if="allowEdit" @click="openChapterModal()"> <Button size="sm" v-if="allowEdit" @click="openChapterModal()">
{{ __('Add Chapter') }} {{ __('Add Chapter') }}
</Button> </Button>
<!-- <span class="font-medium cursor-pointer" @click="expandAllChapters()">
{{ expandAll ? __("Collapse all chapters") : __("Expand all chapters") }}
</span> -->
</div> </div>
<div <div
:class="{ :class="{

View File

@@ -0,0 +1,16 @@
<template>
<svg
width="20"
height="20"
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

@@ -653,3 +653,8 @@ const getSubmissionColumns = () => {
] ]
} }
</script> </script>
<style>
p {
line-height: 1.5rem;
}
</style>

View File

@@ -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-4 pl-4.5"
style="
background: radial-gradient(
circle,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0.4) 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)
@@ -147,7 +169,6 @@ const toggleFullscreen = () => {
<style scoped> <style scoped>
.video-block { .video-block {
width: 100%; width: 100%;
max-width: 900px;
margin: 0 auto; margin: 0 auto;
} }
@@ -165,15 +186,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 +208,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>

View File

@@ -69,7 +69,7 @@
<CourseCardOverlay :course="course" class="md:hidden mb-4" /> <CourseCardOverlay :course="course" class="md:hidden mb-4" />
<div <div
v-html="course.data.description" v-html="course.data.description"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-4" class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-10"
></div> ></div>
<div class="mt-10"> <div class="mt-10">
<CourseOutline <CourseOutline

View File

@@ -57,7 +57,7 @@
</div> </div>
<div <div
v-if="courses.data?.length" v-if="courses.data?.length"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-5" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-4"
> >
<router-link <router-link
v-for="course in courses.data" v-for="course in courses.data"

View File

@@ -4,7 +4,16 @@
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5" class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs class="h-7" :items="breadcrumbs" /> <Breadcrumbs class="h-7" :items="breadcrumbs" />
<div class="flex items-center space-x-2">
<Tooltip v-if="lesson.data?.membership" :text="__('Zen Mode')">
<Button @click="goFullScreen()">
<template #icon>
<Focus class="w-4 h-4 stroke-2" />
</template>
</Button>
</Tooltip>
<CertificationLinks :courseName="courseName" /> <CertificationLinks :courseName="courseName" />
</div>
</header> </header>
<div class="grid md:grid-cols-[70%,30%] h-screen"> <div class="grid md:grid-cols-[70%,30%] h-screen">
<div v-if="lesson.data.no_preview" class="border-r"> <div v-if="lesson.data.no_preview" class="border-r">
@@ -33,13 +42,52 @@
</Button> </Button>
</div> </div>
</div> </div>
<div
<div v-else class="border-r container pt-5 pb-10 px-5"> v-else
<div class="flex flex-col md:flex-row md:items-center justify-between"> ref="lessonContainer"
class="bg-surface-white"
:class="{
'overflow-y-auto': zenModeEnabled,
}"
>
<div
class="border-r container pt-5 pb-10 px-5 h-full"
:class="{
'w-full md:w-3/4 mx-auto border-none !pt-10': zenModeEnabled,
}"
>
<div
class="flex flex-col md:flex-row md:items-center justify-between"
>
<div class="flex flex-col">
<div class="text-3xl font-semibold text-ink-gray-9"> <div class="text-3xl font-semibold text-ink-gray-9">
{{ lesson.data.title }} {{ lesson.data.title }}
</div> </div>
<div class="flex items-center mt-2 md:mt-0">
<div
v-if="zenModeEnabled"
class="relative flex items-center space-x-2 text-sm mt-1 text-ink-gray-7 group w-fit mt-2"
>
<span>
{{ lesson.data.chapter_title }} -
{{ lesson.data.course_title }}
</span>
<Info class="size-3" />
<div
class="hidden group-hover:block rounded bg-gray-900 px-2 py-1 text-xs text-white shadow-xl absolute left-0 top-full mt-2"
>
{{ Math.ceil(lesson.data.membership.progress) }}%
{{ __('completed') }}
</div>
</div>
</div>
<div class="flex items-center space-x-2 mt-2 md:mt-0">
<Button v-if="zenModeEnabled" @click="showDiscussionsInZenMode">
<template #icon>
<MessageCircleQuestion class="w-4 h-4 stroke-1.5" />
</template>
</Button>
<router-link <router-link
v-if="lesson.data.prev" v-if="lesson.data.prev"
:to="{ :to="{
@@ -51,7 +99,7 @@
}, },
}" }"
> >
<Button class="mr-2"> <Button>
<template #prefix> <template #prefix>
<ChevronLeft class="w-4 h-4 stroke-1" /> <ChevronLeft class="w-4 h-4 stroke-1" />
</template> </template>
@@ -71,7 +119,7 @@
}, },
}" }"
> >
<Button class="mr-2"> <Button>
{{ __('Edit') }} {{ __('Edit') }}
</Button> </Button>
</router-link> </router-link>
@@ -109,7 +157,7 @@
</div> </div>
</div> </div>
<div class="flex items-center mt-2"> <div v-if="!zenModeEnabled" class="flex items-center mt-2">
<span <span
class="h-6 mr-1" class="h-6 mr-1"
:class="{ :class="{
@@ -126,6 +174,7 @@
:instructors="lesson.data.instructors" :instructors="lesson.data.instructors"
/> />
</div> </div>
<div <div
v-if=" v-if="
lesson.data.instructor_content && lesson.data.instructor_content &&
@@ -144,19 +193,19 @@
</div> </div>
<div <div
v-else-if="lesson.data.instructor_notes" v-else-if="lesson.data.instructor_notes"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-6" class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
> >
<LessonContent :content="lesson.data.instructor_notes" /> <LessonContent :content="lesson.data.instructor_notes" />
</div> </div>
<div <div
v-if="lesson.data.content" v-if="lesson.data.content"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-5" class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
> >
<div id="editor"></div> <div id="editor"></div>
</div> </div>
<div <div
v-else v-else
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-5" class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
> >
<LessonContent <LessonContent
v-if="lesson.data?.body" v-if="lesson.data?.body"
@@ -165,7 +214,7 @@
:quizId="lesson.data.quiz_id" :quizId="lesson.data.quiz_id"
/> />
</div> </div>
<div class="mt-20"> <div class="mt-20" ref="discussionsContainer">
<Discussions <Discussions
v-if="allowDiscussions" v-if="allowDiscussions"
:title="'Questions'" :title="'Questions'"
@@ -175,6 +224,7 @@
/> />
</div> </div>
</div> </div>
</div>
<div class="sticky top-10"> <div class="sticky top-10">
<div class="bg-surface-menu-bar py-5 px-2 border-b"> <div class="bg-surface-menu-bar py-5 px-2 border-b">
<div class="text-lg font-semibold text-ink-gray-9"> <div class="text-lg font-semibold text-ink-gray-9">
@@ -202,8 +252,22 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource, Breadcrumbs, Button, usePageMeta } from 'frappe-ui' import {
import { computed, watch, inject, ref, onMounted, onBeforeUnmount } from 'vue' createResource,
Breadcrumbs,
Button,
Tooltip,
usePageMeta,
} from 'frappe-ui'
import {
computed,
watch,
inject,
ref,
onMounted,
onBeforeUnmount,
nextTick,
} from 'vue'
import CourseOutline from '@/components/CourseOutline.vue' import CourseOutline from '@/components/CourseOutline.vue'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
@@ -212,6 +276,9 @@ import {
ChevronRight, ChevronRight,
LockKeyholeIcon, LockKeyholeIcon,
LogIn, LogIn,
Focus,
Info,
MessageCircleQuestion,
} from 'lucide-vue-next' } from 'lucide-vue-next'
import Discussions from '@/components/Discussions.vue' import Discussions from '@/components/Discussions.vue'
import { getEditorTools } from '../utils' import { getEditorTools } from '../utils'
@@ -221,6 +288,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()
@@ -229,6 +298,10 @@ const allowDiscussions = ref(false)
const editor = ref(null) const editor = ref(null)
const instructorEditor = ref(null) const instructorEditor = ref(null)
const lessonProgress = ref(0) const lessonProgress = ref(0)
const lessonContainer = ref(null)
const zenModeEnabled = ref(false)
const hasQuiz = ref(false)
const discussionsContainer = ref(null)
const timer = ref(0) const timer = ref(0)
const { brand } = sessionStore() const { brand } = sessionStore()
let timerInterval let timerInterval
@@ -250,11 +323,59 @@ const props = defineProps({
onMounted(() => { onMounted(() => {
startTimer() startTimer()
enablePlyr()
document.addEventListener('fullscreenchange', attachFullscreenEvent)
}) })
const attachFullscreenEvent = () => {
if (document.fullscreenElement) {
zenModeEnabled.value = true
allowDiscussions.value = false
} else {
zenModeEnabled.value = false
if (!hasQuiz.value) {
allowDiscussions.value = true
}
}
}
onBeforeUnmount(() => {
document.removeEventListener('fullscreenchange', attachFullscreenEvent)
})
const enablePlyr = () => {
setTimeout(() => {
const videoElement = document.getElementsByClassName('video-player')
if (videoElement.length === 0) return
const 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],
makeParams(values) { makeParams(values) {
return { return {
course: props.courseName, course: props.courseName,
@@ -263,7 +384,9 @@ const lesson = createResource({
} }
}, },
auto: true, auto: true,
onSuccess(data) { })
const setupLesson = (data) => {
if (Object.keys(data).length === 0) { if (Object.keys(data).length === 0) {
router.push({ router.push({
name: 'CourseDetail', name: 'CourseDetail',
@@ -287,11 +410,10 @@ const lesson = createResource({
if (!editor.value && data.body) { if (!editor.value && data.body) {
const quizRegex = /\{\{ Quiz\(".*"\) \}\}/ const quizRegex = /\{\{ Quiz\(".*"\) \}\}/
const hasQuiz = quizRegex.test(data.body) hasQuiz.value = quizRegex.test(data.body)
if (!hasQuiz) allowDiscussions.value = true if (!hasQuiz.value) allowDiscussions.value = true
}
} }
},
})
const renderEditor = (holder, content) => { const renderEditor = (holder, content) => {
// empty the holder // empty the holder
@@ -362,10 +484,18 @@ watch(
clearInterval(timerInterval) clearInterval(timerInterval)
timer.value = 0 timer.value = 0
startTimer() startTimer()
enablePlyr()
} }
} }
) )
watch(
() => lesson.data,
(data) => {
setupLesson(data)
}
)
const startTimer = () => { const startTimer = () => {
timerInterval = setInterval(() => { timerInterval = setInterval(() => {
timer.value++ timer.value++
@@ -381,13 +511,13 @@ onBeforeUnmount(() => {
}) })
const checkIfDiscussionsAllowed = () => { const checkIfDiscussionsAllowed = () => {
let quizPresent = false
JSON.parse(lesson.data?.content)?.blocks?.forEach((block) => { JSON.parse(lesson.data?.content)?.blocks?.forEach((block) => {
if (block.type === 'quiz') quizPresent = true if (block.type === 'quiz') hasQuiz.value = true
}) })
if ( if (
!quizPresent && !hasQuiz.value &&
!zenModeEnabled.value &&
(lesson.data?.membership || (lesson.data?.membership ||
user.data?.is_moderator || user.data?.is_moderator ||
user.data?.is_instructor) user.data?.is_instructor)
@@ -431,6 +561,37 @@ const enrollStudent = () => {
) )
} }
const goFullScreen = () => {
if (lessonContainer.value.requestFullscreen) {
lessonContainer.value.requestFullscreen()
} else if (lessonContainer.value.mozRequestFullScreen) {
lessonContainer.value.mozRequestFullScreen()
} else if (lessonContainer.value.webkitRequestFullscreen) {
lessonContainer.value.webkitRequestFullscreen()
} else if (lessonContainer.value.msRequestFullscreen) {
lessonContainer.value.msRequestFullscreen()
}
}
const showDiscussionsInZenMode = () => {
if (allowDiscussions.value) {
allowDiscussions.value = false
} else {
allowDiscussions.value = true
scrollDiscussionsIntoView()
}
}
const scrollDiscussionsIntoView = () => {
nextTick(() => {
discussionsContainer.value?.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
})
})
}
const redirectToLogin = () => { const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}` window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}`
} }
@@ -604,4 +765,30 @@ usePageMeta(() => {
.tc-table { .tc-table {
border-left: 1px solid #e8e8eb; border-left: 1px solid #e8e8eb;
} }
.plyr__volume input[type='range'] {
display: none;
}
.plyr__control--overlaid {
background: radial-gradient(
circle,
rgba(0, 0, 0, 0.4) 0%,
rgba(0, 0, 0, 0.5) 50%
);
}
.plyr__control:hover {
background: none;
}
.plyr--video {
border: 1px solid theme('colors.gray.200');
border-radius: 8px;
}
:root {
--plyr-range-fill-background: white;
--plyr-video-control-background-hover: transparent;
}
</style> </style>

View File

@@ -92,14 +92,17 @@ import {
inject, inject,
ref, ref,
onBeforeUnmount, onBeforeUnmount,
watch,
} from 'vue' } from 'vue'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import EditorJS from '@editorjs/editorjs' import EditorJS from '@editorjs/editorjs'
import LessonHelp from '@/components/LessonHelp.vue' import LessonHelp from '@/components/LessonHelp.vue'
import { ChevronRight } from 'lucide-vue-next' import { AppleIcon, ChevronRight } from 'lucide-vue-next'
import { createToast, getEditorTools } from '@/utils' import { createToast, getEditorTools } from '@/utils'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import Plyr from 'plyr'
import 'plyr/dist/plyr.css'
const { brand } = sessionStore() const { brand } = sessionStore()
const editor = ref(null) const editor = ref(null)
@@ -133,6 +136,7 @@ onMounted(() => {
editor.value = renderEditor('content') editor.value = renderEditor('content')
instructorEditor.value = renderEditor('instructor-notes') instructorEditor.value = renderEditor('instructor-notes')
window.addEventListener('keydown', keyboardShortcut) window.addEventListener('keydown', keyboardShortcut)
enablePlyr()
}) })
const renderEditor = (holder) => { const renderEditor = (holder) => {
@@ -141,6 +145,9 @@ const renderEditor = (holder) => {
tools: getEditorTools(true), tools: getEditorTools(true),
autofocus: true, autofocus: true,
defaultBlock: 'markdown', defaultBlock: 'markdown',
onChange: async (api, event) => {
enablePlyr()
},
}) })
} }
@@ -463,6 +470,37 @@ const showToast = (title, text, icon) => {
}) })
} }
const enablePlyr = () => {
setTimeout(() => {
const videoElement = document.getElementsByClassName('video-player')
if (videoElement.length === 0) return
const 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 breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
let crumbs = [ let crumbs = [
{ {
@@ -639,4 +677,30 @@ iframe {
.ce-popover-item[data-item-name='markdown'] { .ce-popover-item[data-item-name='markdown'] {
display: none !important; display: none !important;
} }
.plyr__volume input[type='range'] {
display: none;
}
.plyr__control--overlaid {
background: radial-gradient(
circle,
rgba(0, 0, 0, 0.4) 0%,
rgba(0, 0, 0, 0.5) 50%
);
}
.plyr__control:hover {
background: none;
}
.plyr--video {
border: 1px solid theme('colors.gray.200');
border-radius: 8px;
}
:root {
--plyr-range-fill-background: white;
--plyr-video-control-background-hover: transparent;
}
</style> </style>

View File

@@ -87,7 +87,6 @@ import { sessionStore } from '@/stores/session'
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
const { brand } = sessionStore() const { brand } = sessionStore()
console.log(user.data?.sitename)
const persona = reactive({ const persona = reactive({
role: null, role: null,

View File

@@ -199,63 +199,15 @@ 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 %>', /* '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: `<iframe style="width:100%; height: ${ html: `<div class="video-player" data-plyr-provider="youtube"></div>`,
window.innerWidth < 640 ? '15rem' : '30rem' id: ([id]) => id,
};" 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+)/,
embedUrl: embedUrl: '<%= remote_id %>',
'https://player.vimeo.com/video/<%= remote_id %>', html: `<div class="video-player" data-plyr-provider="vimeo"></div>`,
html: `<iframe style="width:100%; height: ${
window.innerWidth < 640 ? '15rem' : '30rem'
};" frameborder="0" allowfullscreen></iframe>`,
id: ([id]) => id, id: ([id]) => id,
}, },
cloudflareStream: { cloudflareStream: {

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -1314,6 +1314,9 @@ def get_lesson(course, chapter, lesson):
else: else:
progress = get_progress(course, lesson_details.name) progress = get_progress(course, lesson_details.name)
lesson_details.chapter_title = frappe.db.get_value(
"Course Chapter", chapter_name, "title"
)
lesson_details.rendered_content = render_html(lesson_details) lesson_details.rendered_content = render_html(lesson_details)
neighbours = get_neighbour_lesson(course, chapter, lesson) neighbours = get_neighbour_lesson(course, chapter, lesson)
lesson_details.next = neighbours["next"] lesson_details.next = neighbours["next"]

5501
yarn.lock

File diff suppressed because it is too large Load Diff