feat: display quiz when time is reached
This commit is contained in:
@@ -3,23 +3,14 @@
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Add quiz to this video'),
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: __('Save'),
|
||||
variant: 'solid',
|
||||
onClick({ close }) {
|
||||
addQuizToVideo(close)
|
||||
},
|
||||
},
|
||||
],
|
||||
size: '2xl',
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="text-base">
|
||||
<div class="flex items-end gap-4">
|
||||
<FormControl
|
||||
:label="__('Time in Video (seconds)')"
|
||||
:label="__('Time in Video (minutes)')"
|
||||
v-model="quiz.time"
|
||||
type="number"
|
||||
placeholder="Time"
|
||||
@@ -31,7 +22,7 @@
|
||||
doctype="LMS Quiz"
|
||||
class="flex-1"
|
||||
/>
|
||||
<Button @click="addQuiz()">
|
||||
<Button @click="addQuiz()" variant="solid">
|
||||
<template #prefix>
|
||||
<Plus class="w-4 h-4 stroke-1.5" />
|
||||
</template>
|
||||
@@ -39,43 +30,63 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ListView
|
||||
v-if="quizzes.length"
|
||||
class="mt-10"
|
||||
:columns="columns"
|
||||
:rows="allQuizzes"
|
||||
row-key="name"
|
||||
:options="{
|
||||
showTooltip: false,
|
||||
selectable: false,
|
||||
}"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
<div class="mt-10 mb-5">
|
||||
<div class="font-medium mb-4">
|
||||
{{ __('Quizzes in this video') }}
|
||||
</div>
|
||||
<ListView
|
||||
v-if="allQuizzes.length"
|
||||
:columns="columns"
|
||||
:rows="allQuizzes"
|
||||
row-key="quiz"
|
||||
:options="{
|
||||
showTooltip: false,
|
||||
}"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in columns">
|
||||
<template #prefix="{ item }">
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="h-4 w-4 stroke-1.5 ml-4"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in columns">
|
||||
<template #prefix="{ item }">
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="h-4 w-4 stroke-1.5 ml-4"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
|
||||
<ListRows>
|
||||
<ListRow :row="row" v-for="row in allQuizzes">
|
||||
<template #default="{ column, item }">
|
||||
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||
<div class="leading-5 text-sm">
|
||||
{{ row[column.key] }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
<ListRows>
|
||||
<ListRow :row="row" v-for="row in allQuizzes">
|
||||
<template #default="{ column, item }">
|
||||
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||
<div class="leading-5 text-sm">
|
||||
{{ row[column.key] }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</template>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
|
||||
<ListSelectBanner>
|
||||
<template #actions="{ unselectAll, selections }">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@click="removeQuiz(selections, unselectAll)"
|
||||
>
|
||||
<Trash2 class="h-4 w-4 stroke-1.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
</ListView>
|
||||
</ListSelectBanner>
|
||||
</ListView>
|
||||
|
||||
<div v-else class="text-ink-gray-5 italic text-xs">
|
||||
{{ __('No quizzes added yet.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -91,10 +102,11 @@ import {
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListRowItem,
|
||||
ListSelectBanner,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
type Quiz = {
|
||||
@@ -124,11 +136,6 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const addQuizToVideo = (close: () => void) => {
|
||||
props.saveQuizzes(allQuizzes.value)
|
||||
close()
|
||||
}
|
||||
|
||||
const addQuiz = () => {
|
||||
if (quiz.time > props.duration) {
|
||||
toast.error(__('Time in video exceeds the total duration of the video.'))
|
||||
@@ -139,10 +146,23 @@ const addQuiz = () => {
|
||||
quiz: quiz.quiz,
|
||||
})
|
||||
|
||||
props.saveQuizzes(allQuizzes.value)
|
||||
|
||||
quiz.time = 0
|
||||
quiz.quiz = ''
|
||||
}
|
||||
|
||||
const removeQuiz = (selections: string, unselectAll: () => void) => {
|
||||
Array.from(selections).forEach((selection) => {
|
||||
const index = allQuizzes.value.findIndex((q) => q.quiz === selection)
|
||||
if (index !== -1) {
|
||||
allQuizzes.value.splice(index, 1)
|
||||
}
|
||||
unselectAll()
|
||||
})
|
||||
props.saveQuizzes(allQuizzes.value)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.quizzes,
|
||||
(newQuizzes) => {
|
||||
@@ -159,7 +179,7 @@ const columns = computed(() => {
|
||||
},
|
||||
{
|
||||
key: 'time',
|
||||
label: __('Time in Video (seconds)'),
|
||||
label: __('Time in Video (minutes)'),
|
||||
align: 'center',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<div v-if="quiz.data">
|
||||
<div
|
||||
class="bg-surface-blue-2 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-ink-blue-3"
|
||||
class="bg-surface-blue-2 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-ink-blue-3 leading-5"
|
||||
>
|
||||
<div v-if="inVideo">
|
||||
{{ __('You will have to complete the quiz to continue the video') }}
|
||||
</div>
|
||||
<div class="leading-5">
|
||||
{{
|
||||
__('This quiz consists of {0} questions.').format(questions.length)
|
||||
@@ -247,18 +250,23 @@
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<Button
|
||||
@click="resetQuiz()"
|
||||
class="mt-2"
|
||||
v-if="
|
||||
!quiz.data.max_attempts ||
|
||||
attempts?.data.length < quiz.data.max_attempts
|
||||
"
|
||||
>
|
||||
<span>
|
||||
{{ __('Try Again') }}
|
||||
</span>
|
||||
</Button>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
@click="resetQuiz()"
|
||||
class="mt-2"
|
||||
v-if="
|
||||
!quiz.data.max_attempts ||
|
||||
attempts?.data.length < quiz.data.max_attempts
|
||||
"
|
||||
>
|
||||
<span>
|
||||
{{ __('Try Again') }}
|
||||
</span>
|
||||
</Button>
|
||||
<Button v-if="inVideo" @click="props.onSubmit()">
|
||||
{{ __('Resume Video') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
@@ -315,6 +323,14 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
inVideo: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onSubmit: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
const quiz = createResource({
|
||||
|
||||
@@ -293,7 +293,7 @@ const tabsStructure = computed(() => {
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Certified Participants',
|
||||
label: 'Certified Members',
|
||||
name: 'certified_participants',
|
||||
type: 'checkbox',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<div ref="videoContainer" class="video-block relative group">
|
||||
<div
|
||||
v-if="quizzes.length && !showQuiz"
|
||||
class="bg-surface-blue-2 space-y-1 py-3 px-4 rounded-md text-sm text-ink-blue-3 leading-5"
|
||||
>
|
||||
{{
|
||||
__('This video has {0} {1}:').format(
|
||||
quizzes.length,
|
||||
quizzes.length == 1 ? 'quiz' : 'quizzes'
|
||||
)
|
||||
}}
|
||||
|
||||
<div v-for="(quiz, index) in quizzes" class="pl-3 mt-1">
|
||||
<span> {{ index + 1 }}. {{ quiz.quiz }} </span>
|
||||
{{ __('at {0} minutes').format(quiz.time) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!showQuiz"
|
||||
ref="videoContainer"
|
||||
class="video-block relative group"
|
||||
>
|
||||
<video
|
||||
@timeupdate="updateTime"
|
||||
@ended="videoEnded"
|
||||
@@ -78,6 +98,12 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Quiz
|
||||
v-if="showQuiz"
|
||||
:quizName="currentQuiz"
|
||||
:inVideo="true"
|
||||
:onSubmit="resumeVideo"
|
||||
/>
|
||||
<div v-if="!readOnly" @click="showQuizModal = true">
|
||||
<Button>
|
||||
{{ __('Add Quiz to Video') }}
|
||||
@@ -106,6 +132,9 @@ let currentTime = ref(0)
|
||||
let duration = ref(0)
|
||||
let muted = ref(false)
|
||||
const showQuizModal = ref(false)
|
||||
const showQuiz = ref(false)
|
||||
const currentQuiz = ref(null)
|
||||
const nextQuiz = ref({})
|
||||
|
||||
const props = defineProps({
|
||||
file: {
|
||||
@@ -130,15 +159,54 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateCurrentTime()
|
||||
updateNextQuiz()
|
||||
})
|
||||
|
||||
const updateCurrentTime = () => {
|
||||
setTimeout(() => {
|
||||
videoRef.value.onloadedmetadata = () => {
|
||||
duration.value = videoRef.value.duration
|
||||
}
|
||||
videoRef.value.ontimeupdate = () => {
|
||||
currentTime.value = videoRef.value.currentTime
|
||||
currentTime.value = videoRef.value?.currentTime || currentTime.value
|
||||
if (currentTime.value >= nextQuiz.value.time * 60) {
|
||||
videoRef.value.pause()
|
||||
playing.value = false
|
||||
videoRef.value.onTimeupdate = null
|
||||
currentQuiz.value = nextQuiz.value.quiz
|
||||
showQuiz.value = true
|
||||
updateNextQuiz()
|
||||
}
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
}
|
||||
|
||||
const resumeVideo = () => {
|
||||
showQuiz.value = false
|
||||
currentQuiz.value = null
|
||||
updateCurrentTime()
|
||||
setTimeout(() => {
|
||||
videoRef.value.currentTime = currentTime.value
|
||||
videoRef.value.play()
|
||||
playing.value = true
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const updateNextQuiz = () => {
|
||||
if (!props.quizzes.length) return
|
||||
props.quizzes.sort((a, b) => a.time - b.time)
|
||||
|
||||
const nextQuizIndex = props.quizzes.findIndex(
|
||||
(quiz) => quiz.time * 60 > currentTime.value
|
||||
)
|
||||
|
||||
if (nextQuizIndex !== -1) {
|
||||
nextQuiz.value = props.quizzes[nextQuizIndex]
|
||||
} else {
|
||||
nextQuiz.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
const fileURL = computed(() => {
|
||||
if (isYoutube) {
|
||||
@@ -156,6 +224,7 @@ const isYoutube = computed(() => {
|
||||
})
|
||||
|
||||
const playVideo = () => {
|
||||
console.log(currentTime.value)
|
||||
videoRef.value.play()
|
||||
playing.value = true
|
||||
}
|
||||
@@ -166,6 +235,7 @@ const pauseVideo = () => {
|
||||
}
|
||||
|
||||
const togglePlay = () => {
|
||||
console.log(currentTime.value)
|
||||
if (playing.value) {
|
||||
pauseVideo()
|
||||
} else {
|
||||
@@ -184,6 +254,8 @@ const toggleMute = () => {
|
||||
|
||||
const changeCurrentTime = () => {
|
||||
videoRef.value.currentTime = currentTime.value
|
||||
console.log('Current Time:', currentTime.value)
|
||||
updateNextQuiz()
|
||||
}
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
|
||||
@@ -47,6 +47,7 @@ export class Upload {
|
||||
const app = createApp(VideoBlock, {
|
||||
file: file.file_url,
|
||||
readOnly: this.readOnly,
|
||||
quizzes: file.quizzes || [],
|
||||
saveQuizzes: (quizzes) => {
|
||||
if (this.readOnly) return
|
||||
this.data.quizzes = quizzes
|
||||
|
||||
@@ -96,6 +96,11 @@ def get_quiz_progress(lesson):
|
||||
for block in content.get("blocks"):
|
||||
if block.get("type") == "quiz":
|
||||
quizzes.append(block.get("data").get("quiz"))
|
||||
if block.get("type") == "upload":
|
||||
quizzes_in_video = block.get("data").get("quizzes")
|
||||
if quizzes_in_video and len(quizzes_in_video) > 0:
|
||||
for row in quizzes_in_video:
|
||||
quizzes.append(row.get("quiz"))
|
||||
|
||||
elif lesson_details.body:
|
||||
macros = find_macros(lesson_details.body)
|
||||
|
||||
Reference in New Issue
Block a user