From 975399e293726f9f902db059ea61e33bcd9d5875 Mon Sep 17 00:00:00 2001 From: Gigi Date: Mon, 20 Oct 2025 20:37:45 +0200 Subject: [PATCH] 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 --- src/components/ContentPanel.tsx | 19 +-- src/components/Settings.tsx | 1 + .../Settings/ReadingDisplaySettings.tsx | 13 ++ src/components/VideoEmbedProcessor.tsx | 131 ++++++++++++++++++ src/services/settingsService.ts | 1 + 5 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 src/components/VideoEmbedProcessor.tsx diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index 73129d12..cdfabd42 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -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 = ({ <> {markdown ? ( renderedMarkdownHtml && finalHtml ? ( -
@@ -858,10 +860,11 @@ const ContentPanel: React.FC = ({
) ) : ( -
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 4ebdbafe..4cea9196 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -40,6 +40,7 @@ const DEFAULT_SETTINGS: UserSettings = { rebroadcastToAllRelays: false, paragraphAlignment: 'justify', fullWidthImages: false, + renderVideoLinksAsEmbeds: false, syncReadingPosition: true, autoMarkAsReadOnCompletion: false, hideBookmarksWithoutCreationDate: true, diff --git a/src/components/Settings/ReadingDisplaySettings.tsx b/src/components/Settings/ReadingDisplaySettings.tsx index 55f5e25e..32acec73 100644 --- a/src/components/Settings/ReadingDisplaySettings.tsx +++ b/src/components/Settings/ReadingDisplaySettings.tsx @@ -72,6 +72,19 @@ const ReadingDisplaySettings: React.FC = ({ setting
+
+ +
+
diff --git a/src/components/VideoEmbedProcessor.tsx b/src/components/VideoEmbedProcessor.tsx new file mode 100644 index 00000000..825ecd47 --- /dev/null +++ b/src/components/VideoEmbedProcessor.tsx @@ -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(({ + 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 ( +
+ ) + } + + // Split the HTML by video placeholders and render with embedded players + const parts = processedHtml.split(/(__VIDEO_EMBED_\d+__)/) + + return ( +
+ {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 ( +
+ +
+ ) + } + } + + // Regular HTML content + return ( +
+ ) + })} +
+ ) +}) + +VideoEmbedProcessor.displayName = 'VideoEmbedProcessor' + +export default VideoEmbedProcessor diff --git a/src/services/settingsService.ts b/src/services/settingsService.ts index e7b9d466..abc37f4f 100644 --- a/src/services/settingsService.ts +++ b/src/services/settingsService.ts @@ -59,6 +59,7 @@ export interface UserSettings { // Reading settings paragraphAlignment?: 'left' | 'justify' // default: justify fullWidthImages?: boolean // default: false + renderVideoLinksAsEmbeds?: boolean // default: false // Reading position sync syncReadingPosition?: boolean // default: false (opt-in) autoMarkAsReadOnCompletion?: boolean // default: false (opt-in)