feat: add video embed setting and processor

- Add renderVideoLinksAsEmbeds setting to UserSettings interface
- Add checkbox control in ReadingDisplaySettings component
- Create VideoEmbedProcessor component to handle video link embedding
- Integrate VideoEmbedProcessor into ContentPanel for article rendering
- Support .mp4, .webm, .ogg, .mov, .avi, .mkv, .m4v video formats
- Use ReactPlayer for embedded video playback
- Default to false (render as links)
- When enabled, video links are rendered as embedded players
This commit is contained in:
Gigi
2025-10-20 20:37:45 +02:00
parent 53b8356373
commit 975399e293
5 changed files with 157 additions and 8 deletions

View File

@@ -4,6 +4,7 @@ import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import rehypeRaw from 'rehype-raw'
import rehypePrism from 'rehype-prism-plus'
import VideoEmbedProcessor from './VideoEmbedProcessor'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import 'prismjs/themes/prism-tomorrow.css'
import { faSpinner, faCheckCircle, faEllipsisH, faExternalLinkAlt, faMobileAlt, faCopy, faShare, faSearch } from '@fortawesome/free-solid-svg-icons'
@@ -843,10 +844,11 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
<>
{markdown ? (
renderedMarkdownHtml && finalHtml ? (
<div
ref={contentRef}
className="reader-markdown"
dangerouslySetInnerHTML={{ __html: finalHtml }}
<VideoEmbedProcessor
ref={contentRef}
html={finalHtml}
renderVideoLinksAsEmbeds={settings?.renderVideoLinksAsEmbeds === true}
className="reader-markdown"
onMouseUp={handleSelectionEnd}
onTouchEnd={handleSelectionEnd}
/>
@@ -858,10 +860,11 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
</div>
)
) : (
<div
ref={contentRef}
className="reader-html"
dangerouslySetInnerHTML={{ __html: finalHtml || html || '' }}
<VideoEmbedProcessor
ref={contentRef}
html={finalHtml || html || ''}
renderVideoLinksAsEmbeds={settings?.renderVideoLinksAsEmbeds === true}
className="reader-html"
onMouseUp={handleSelectionEnd}
onTouchEnd={handleSelectionEnd}
/>

View File

@@ -40,6 +40,7 @@ const DEFAULT_SETTINGS: UserSettings = {
rebroadcastToAllRelays: false,
paragraphAlignment: 'justify',
fullWidthImages: false,
renderVideoLinksAsEmbeds: false,
syncReadingPosition: true,
autoMarkAsReadOnCompletion: false,
hideBookmarksWithoutCreationDate: true,

View File

@@ -72,6 +72,19 @@ const ReadingDisplaySettings: React.FC<ReadingDisplaySettingsProps> = ({ setting
</label>
</div>
<div className="setting-group">
<label htmlFor="renderVideoLinksAsEmbeds" className="checkbox-label">
<input
id="renderVideoLinksAsEmbeds"
type="checkbox"
checked={settings.renderVideoLinksAsEmbeds === true}
onChange={(e) => onUpdate({ renderVideoLinksAsEmbeds: e.target.checked })}
className="setting-checkbox"
/>
<span>Render video links as embeds</span>
</label>
</div>
<div className="setting-group setting-inline">
<label>Default Highlight Visibility</label>
<div className="highlight-level-toggles">

View File

@@ -0,0 +1,131 @@
import React, { useMemo, forwardRef } from 'react'
import ReactPlayer from 'react-player'
import { classifyUrl } from '../utils/helpers'
interface VideoEmbedProcessorProps {
html: string
renderVideoLinksAsEmbeds: boolean
className?: string
onMouseUp?: (e: React.MouseEvent) => void
onTouchEnd?: (e: React.TouchEvent) => void
}
/**
* Component that processes HTML content and optionally embeds video links
* as ReactPlayer components when renderVideoLinksAsEmbeds is enabled
*/
const VideoEmbedProcessor = forwardRef<HTMLDivElement, VideoEmbedProcessorProps>(({
html,
renderVideoLinksAsEmbeds,
className,
onMouseUp,
onTouchEnd
}, ref) => {
const processedHtml = useMemo(() => {
if (!renderVideoLinksAsEmbeds || !html) {
return html
}
// Find all video URLs in the HTML content
const videoUrlPattern = /https?:\/\/[^\s<>"']+\.(mp4|webm|ogg|mov|avi|mkv|m4v)(?:\?[^\s<>"']*)?/gi
const videoUrls = html.match(videoUrlPattern) || []
// Also check for video URLs that might not have extensions but are classified as video
const allUrlPattern = /https?:\/\/[^\s<>"']+/gi
const allUrls = html.match(allUrlPattern) || []
const videoUrlsWithoutExt = allUrls.filter(url => {
const classification = classifyUrl(url)
return classification.type === 'video' && !videoUrls.includes(url)
})
const allVideoUrls = [...videoUrls, ...videoUrlsWithoutExt]
if (allVideoUrls.length === 0) {
return html
}
// Replace video URLs with placeholder divs that we'll replace with ReactPlayer
let processedHtml = html
allVideoUrls.forEach((url, index) => {
const placeholder = `__VIDEO_EMBED_${index}__`
processedHtml = processedHtml.replace(new RegExp(url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), placeholder)
})
return processedHtml
}, [html, renderVideoLinksAsEmbeds])
const videoUrls = useMemo(() => {
if (!renderVideoLinksAsEmbeds || !html) {
return []
}
const videoUrlPattern = /https?:\/\/[^\s<>"']+\.(mp4|webm|ogg|mov|avi|mkv|m4v)(?:\?[^\s<>"']*)?/gi
const videoUrls = html.match(videoUrlPattern) || []
const allUrlPattern = /https?:\/\/[^\s<>"']+/gi
const allUrls = html.match(allUrlPattern) || []
const videoUrlsWithoutExt = allUrls.filter(url => {
const classification = classifyUrl(url)
return classification.type === 'video' && !videoUrls.includes(url)
})
return [...videoUrls, ...videoUrlsWithoutExt]
}, [html, renderVideoLinksAsEmbeds])
// If no video embedding is enabled, just render the HTML normally
if (!renderVideoLinksAsEmbeds || videoUrls.length === 0) {
return (
<div
ref={ref}
className={className}
dangerouslySetInnerHTML={{ __html: processedHtml }}
onMouseUp={onMouseUp}
onTouchEnd={onTouchEnd}
/>
)
}
// Split the HTML by video placeholders and render with embedded players
const parts = processedHtml.split(/(__VIDEO_EMBED_\d+__)/)
return (
<div ref={ref} className={className} onMouseUp={onMouseUp} onTouchEnd={onTouchEnd}>
{parts.map((part, index) => {
const videoMatch = part.match(/^__VIDEO_EMBED_(\d+)__$/)
if (videoMatch) {
const videoIndex = parseInt(videoMatch[1])
const videoUrl = videoUrls[videoIndex]
if (videoUrl) {
return (
<div key={index} className="reader-video" style={{ margin: '1rem 0' }}>
<ReactPlayer
url={videoUrl}
controls
width="100%"
height="auto"
style={{
width: '100%',
height: 'auto',
aspectRatio: '16/9'
}}
/>
</div>
)
}
}
// Regular HTML content
return (
<div
key={index}
dangerouslySetInnerHTML={{ __html: part }}
/>
)
})}
</div>
)
})
VideoEmbedProcessor.displayName = 'VideoEmbedProcessor'
export default VideoEmbedProcessor