mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: add details to video activities
wip: uploadable video activities
This commit is contained in:
parent
3173e6b417
commit
31b5104dd5
7 changed files with 181 additions and 3 deletions
|
|
@ -3,6 +3,13 @@ import YouTube from 'react-youtube'
|
|||
import { getActivityMediaDirectory } from '@services/media/media'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
|
||||
interface VideoDetails {
|
||||
startTime?: number
|
||||
endTime?: number | null
|
||||
autoplay?: boolean
|
||||
muted?: boolean
|
||||
}
|
||||
|
||||
interface VideoActivityProps {
|
||||
activity: {
|
||||
activity_sub_type: string
|
||||
|
|
@ -11,6 +18,7 @@ interface VideoActivityProps {
|
|||
filename?: string
|
||||
uri?: string
|
||||
}
|
||||
details?: VideoDetails
|
||||
}
|
||||
course: {
|
||||
course_uuid: string
|
||||
|
|
@ -20,6 +28,7 @@ interface VideoActivityProps {
|
|||
function VideoActivity({ activity, course }: VideoActivityProps) {
|
||||
const org = useOrg() as any
|
||||
const [videoId, setVideoId] = React.useState('')
|
||||
const videoRef = React.useRef<HTMLVideoElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activity?.content?.uri) {
|
||||
|
|
@ -39,6 +48,26 @@ function VideoActivity({ activity, course }: VideoActivityProps) {
|
|||
)
|
||||
}
|
||||
|
||||
// Handle native video time update
|
||||
const handleTimeUpdate = () => {
|
||||
const video = videoRef.current
|
||||
if (video && activity.details?.endTime) {
|
||||
if (video.currentTime >= activity.details.endTime) {
|
||||
video.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle native video load
|
||||
const handleVideoLoad = () => {
|
||||
const video = videoRef.current
|
||||
if (video && activity.details) {
|
||||
video.currentTime = activity.details.startTime || 0
|
||||
video.autoplay = activity.details.autoplay || false
|
||||
video.muted = activity.details.muted || false
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-full px-2 sm:px-4">
|
||||
{activity && (
|
||||
|
|
@ -47,9 +76,12 @@ function VideoActivity({ activity, course }: VideoActivityProps) {
|
|||
<div className="my-3 md:my-5 w-full">
|
||||
<div className="relative w-full aspect-video rounded-lg overflow-hidden ring-1 ring-gray-300/30 dark:ring-gray-600/30 sm:ring-gray-200/10 sm:dark:ring-gray-700/20 shadow-xs sm:shadow-none">
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="w-full h-full object-cover"
|
||||
controls
|
||||
src={getVideoSrc()}
|
||||
onLoadedMetadata={handleVideoLoad}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -63,7 +95,10 @@ function VideoActivity({ activity, course }: VideoActivityProps) {
|
|||
width: '100%',
|
||||
height: '100%',
|
||||
playerVars: {
|
||||
autoplay: 0
|
||||
autoplay: activity.details?.autoplay ? 1 : 0,
|
||||
mute: activity.details?.muted ? 1 : 0,
|
||||
start: activity.details?.startTime || 0,
|
||||
end: activity.details?.endTime || undefined
|
||||
},
|
||||
}}
|
||||
videoId={videoId}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,19 @@ import { constructAcceptValue } from '@/lib/constants'
|
|||
|
||||
const SUPPORTED_FILES = constructAcceptValue(['mp4', 'webm'])
|
||||
|
||||
interface VideoDetails {
|
||||
startTime: number
|
||||
endTime: number | null
|
||||
autoplay: boolean
|
||||
muted: boolean
|
||||
}
|
||||
|
||||
interface ExternalVideoObject {
|
||||
name: string
|
||||
type: string
|
||||
uri: string
|
||||
chapter_id: string
|
||||
details: VideoDetails
|
||||
}
|
||||
|
||||
function VideoModal({
|
||||
|
|
@ -32,6 +40,12 @@ function VideoModal({
|
|||
const [name, setName] = React.useState('')
|
||||
const [youtubeUrl, setYoutubeUrl] = React.useState('')
|
||||
const [selectedView, setSelectedView] = React.useState<'file' | 'youtube'>('file')
|
||||
const [videoDetails, setVideoDetails] = React.useState<VideoDetails>({
|
||||
startTime: 0,
|
||||
endTime: null,
|
||||
autoplay: false,
|
||||
muted: false
|
||||
})
|
||||
|
||||
const handleVideoChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files?.[0]) {
|
||||
|
|
@ -56,6 +70,7 @@ function VideoModal({
|
|||
published_version: 1,
|
||||
version: 1,
|
||||
course_id: course.id,
|
||||
details: videoDetails
|
||||
},
|
||||
chapterId
|
||||
)
|
||||
|
|
@ -67,6 +82,7 @@ function VideoModal({
|
|||
type: 'youtube',
|
||||
uri: youtubeUrl,
|
||||
chapter_id: chapterId,
|
||||
details: videoDetails
|
||||
}
|
||||
|
||||
await submitExternalVideo(
|
||||
|
|
@ -80,6 +96,73 @@ function VideoModal({
|
|||
}
|
||||
}
|
||||
|
||||
const VideoSettingsForm = () => (
|
||||
<div className="space-y-4 mt-4 p-4 bg-gray-50 rounded-lg">
|
||||
<h3 className="font-medium text-gray-900 mb-3">Video Settings</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField name="start-time">
|
||||
<FormLabel>Start Time (seconds)</FormLabel>
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
value={videoDetails.startTime}
|
||||
onChange={(e) => setVideoDetails({
|
||||
...videoDetails,
|
||||
startTime: Math.max(0, parseInt(e.target.value) || 0)
|
||||
})}
|
||||
placeholder="0"
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="end-time">
|
||||
<FormLabel>End Time (seconds, optional)</FormLabel>
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
type="number"
|
||||
min={videoDetails.startTime + 1}
|
||||
value={videoDetails.endTime || ''}
|
||||
onChange={(e) => setVideoDetails({
|
||||
...videoDetails,
|
||||
endTime: e.target.value ? parseInt(e.target.value) : null
|
||||
})}
|
||||
placeholder="Leave empty for full duration"
|
||||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6 mt-4">
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={videoDetails.autoplay}
|
||||
onChange={(e) => setVideoDetails({
|
||||
...videoDetails,
|
||||
autoplay: e.target.checked
|
||||
})}
|
||||
className="rounded border-gray-300 text-black focus:ring-black"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Autoplay video</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={videoDetails.muted}
|
||||
onChange={(e) => setVideoDetails({
|
||||
...videoDetails,
|
||||
muted: e.target.checked
|
||||
})}
|
||||
className="rounded border-gray-300 text-black focus:ring-black"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Start muted</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="video-activity-name">
|
||||
|
|
@ -143,6 +226,7 @@ function VideoModal({
|
|||
/>
|
||||
</div>
|
||||
</FormField>
|
||||
<VideoSettingsForm />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -160,6 +244,7 @@ function VideoModal({
|
|||
/>
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<VideoSettingsForm />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue