mirror of
https://github.com/nicetry247/offlineacademy.git
synced 2026-08-19 06:57:57 +00:00
feat: add multilingual subtitle tracks
This commit is contained in:
@@ -103,7 +103,7 @@ const liveGroups: FeatureGroup[] = [
|
||||
items: [
|
||||
'Bookmarks with timestamp notes',
|
||||
'Jump-to-time bookmark playback',
|
||||
'Subtitle support for .srt and .vtt',
|
||||
'Multi-language subtitle tracks for .vtt/.srt files',
|
||||
'Inline document viewing for lessons',
|
||||
],
|
||||
},
|
||||
|
||||
+4
-2
@@ -182,7 +182,8 @@ export default function ScanPage() {
|
||||
├── Course Name 1/
|
||||
│ ├── 01 - Introduction/
|
||||
│ │ ├── 01 - Introduction.mp4
|
||||
│ │ ├── 01 - Introduction.srt
|
||||
│ │ ├── 01 - Introduction.en.vtt
|
||||
│ │ ├── 01 - Introduction.es.vtt
|
||||
│ │ ├── 02 - Overview.pdf
|
||||
│ │ └── 03 - Notes.txt
|
||||
│ ├── 02 - Advanced/
|
||||
@@ -204,7 +205,8 @@ export default function ScanPage() {
|
||||
</pre>
|
||||
<div className="mt-3 space-y-2 text-sm text-muted-foreground">
|
||||
<p><strong>How the scanner reads it:</strong> course folder → module folder → lesson files.</p>
|
||||
<p><strong>Matching rule:</strong> subtitle files should share the same base name as the video, like <code className="bg-background px-1.5 py-0.5 rounded">lesson.mp4</code> + <code className="bg-background px-1.5 py-0.5 rounded">lesson.srt</code>.</p>
|
||||
<p><strong>Matching rule:</strong> subtitle files can either share the exact video base name, like <code className="bg-background px-1.5 py-0.5 rounded">lesson.mp4</code> + <code className="bg-background px-1.5 py-0.5 rounded">lesson.vtt</code>, or use language suffixes for multiple tracks, like <code className="bg-background px-1.5 py-0.5 rounded">lesson.en.vtt</code> + <code className="bg-background px-1.5 py-0.5 rounded">lesson.es.vtt</code>.</p>
|
||||
<p><strong>Subtitle formats:</strong> <code className="bg-background px-1.5 py-0.5 rounded">.vtt</code> is preferred for browser playback; <code className="bg-background px-1.5 py-0.5 rounded">.srt</code> files are detected and listed as best-effort tracks.</p>
|
||||
<p><strong>Sorting:</strong> numeric prefixes like <code className="bg-background px-1.5 py-0.5 rounded">01</code>, <code className="bg-background px-1.5 py-0.5 rounded">02</code>, <code className="bg-background px-1.5 py-0.5 rounded">10</code> keep lessons in learning order.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -21,6 +21,15 @@ import Link from 'next/link'
|
||||
import { formatDuration, formatTime, cn } from '@/lib/utils'
|
||||
import { getCourseDisplayName } from '@/lib/course-display'
|
||||
|
||||
interface SubtitleTrackType {
|
||||
id?: string
|
||||
src: string
|
||||
lang: string
|
||||
label: string
|
||||
format?: string
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
interface LessonType {
|
||||
id: string
|
||||
title: string
|
||||
@@ -33,6 +42,7 @@ interface LessonType {
|
||||
filePath: string
|
||||
thumbnail: string | null
|
||||
subtitlePath: string | null
|
||||
subtitles?: SubtitleTrackType[]
|
||||
progress?: { completed: boolean; position: number; lastWatched: string } | null
|
||||
quiz?: {
|
||||
version: 1
|
||||
@@ -118,13 +128,28 @@ function markCompleteFn(lessonId: string, courseId: string, moduleId: string, po
|
||||
}
|
||||
|
||||
function VideoContent({ lesson, lessonProgress, onSave, onEnd, onTimeUpdate, seekRequest }: {
|
||||
lesson: { id: string; filePath: string; thumbnail: string | null; duration: number | null; subtitlePath: string | null }
|
||||
lesson: { id: string; filePath: string; thumbnail: string | null; duration: number | null; subtitlePath: string | null; subtitles?: SubtitleTrackType[] }
|
||||
lessonProgress: { completed: boolean; position: number; lastWatched: string }
|
||||
onSave: (position: number) => void
|
||||
onEnd: () => void
|
||||
onTimeUpdate: (time: number) => void
|
||||
seekRequest: { time: number; nonce: number } | null
|
||||
}) {
|
||||
const subtitleTracks = React.useMemo(() => {
|
||||
if (lesson.subtitles && lesson.subtitles.length > 0) {
|
||||
return lesson.subtitles.map(track => ({
|
||||
src: '/api/files/' + track.src,
|
||||
srcLang: track.lang,
|
||||
label: track.label,
|
||||
default: track.isDefault,
|
||||
}))
|
||||
}
|
||||
|
||||
return lesson.subtitlePath
|
||||
? [{ src: '/api/files/' + lesson.subtitlePath, srcLang: 'en', label: 'English', default: true }]
|
||||
: []
|
||||
}, [lesson.subtitlePath, lesson.subtitles])
|
||||
|
||||
return (
|
||||
<VideoPlayer
|
||||
src={'/api/files/' + lesson.filePath}
|
||||
@@ -135,7 +160,7 @@ function VideoContent({ lesson, lessonProgress, onSave, onEnd, onTimeUpdate, see
|
||||
onProgressSave={onSave}
|
||||
onEnded={onEnd}
|
||||
seekRequest={seekRequest}
|
||||
subtitles={lesson.subtitlePath ? '/api/files/' + lesson.subtitlePath : undefined}
|
||||
subtitles={subtitleTracks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ export default async function WatchPage({ params, searchParams }: WatchPageProps
|
||||
progress: {
|
||||
where: { userId: 'local-user' },
|
||||
},
|
||||
subtitles: {
|
||||
orderBy: [
|
||||
{ isDefault: 'desc' },
|
||||
{ label: 'asc' },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user