merge main

This commit is contained in:
Markos Gogoulos
2026-02-06 19:34:28 +02:00
26 changed files with 8307 additions and 331 deletions

View File

@@ -0,0 +1,22 @@
name: "Lint PR"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
- reopened
permissions:
pull-requests: read
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
environment: dev
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

47
.github/workflows/semantic-release.yaml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Semantic Release
on:
push:
branches:
- main
permissions:
contents: write
issues: write
jobs:
semantic-release:
runs-on: ubuntu-latest
environment: dev
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Setup SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.GA_DEPLOY_KEY }}
# use SSH url to ensure git commit using a deploy key bypasses the main
# branch protection rule
- name: Configure Git for SSH Push
run: git remote set-url origin "git@github.com:${{ github.repository }}.git"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
- name: Install Dependencies
run: npm clean-install
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
run: npm audit signatures
- name: Run Semantic Release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,3 +1,4 @@
/templates/cms/* /templates/cms/*
/templates/*.html /templates/*.html
*.scss *.scss
/frontend/

100
.releaserc.json Normal file
View File

@@ -0,0 +1,100 @@
{
"branches": [
"main"
],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "chore",
"hidden": true
},
{
"type": "docs",
"section": "Documentation"
},
{
"type": "style",
"hidden": true
},
{
"type": "refactor",
"section": "Refactors"
},
{
"type": "perf",
"section": "Performance"
},
{
"type": "test",
"hidden": true
},
{
"type": "depr",
"section": "Deprecations"
}
]
}
}
],
[
"semantic-release-replace-plugin",
{
"replacements": [
{
"files": [
"package.json"
],
"from": "\"version\": \".*\"",
"to": "\"version\": \"${nextRelease.version}\"",
"results": [
{
"file": "package.json",
"hasChanged": true,
"numMatches": 1,
"numReplacements": 1
}
],
"countMatches": true
}
]
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md",
"changelogTitle": "# Changelog"
}
],
"@semantic-release/github",
[
"@semantic-release/git",
{
"assets": [
"package.json",
"CHANGELOG.md"
],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}

37
CHANGELOG.md Normal file
View File

@@ -0,0 +1,37 @@
# Changelog
## [7.5.0](https://github.com/mediacms-io/mediacms/compare/v7.4.0...v7.5.0) (2026-02-06)
### Features
* bump version ([36d815c](https://github.com/mediacms-io/mediacms/commit/36d815c0cfbe21d3136541d410d545742b9ebecd))
## [7.4.0](https://github.com/mediacms-io/mediacms/compare/v7.3.0...v7.4.0) (2026-02-06)
### Features
* Add video player context menu with share/embed options ([#1472](https://github.com/mediacms-io/mediacms/issues/1472)) ([74952f6](https://github.com/mediacms-io/mediacms/commit/74952f68d79bc67617edb38eac62d2f5e7457565))
## [7.3.0](https://github.com/mediacms-io/mediacms/compare/v7.2.0...v7.3.0) (2026-02-06)
### Features
* add package json for semantic release ([b405a04](https://github.com/mediacms-io/mediacms/commit/b405a04e346ca81b7d3f4e099eb984e7785cdd0f))
* add semantic release github actions ([76a27ae](https://github.com/mediacms-io/mediacms/commit/76a27ae25609178c1bd47c947b9f1a082c791d61))
* frontend unit tests ([1c15880](https://github.com/mediacms-io/mediacms/commit/1c15880ae3ef1ce77f53d5b473dfc0cc448b4977))
* Implement persistent "Embed Mode" to hide UI shell via Session Storage ([#1484](https://github.com/mediacms-io/mediacms/issues/1484)) ([223e870](https://github.com/mediacms-io/mediacms/commit/223e87073f7d5e44130c9976854cac670db0ae66))
* Improve Visual Distinction Between Trim and Chapters Editors ([#1445](https://github.com/mediacms-io/mediacms/issues/1445)) ([d9b1d6c](https://github.com/mediacms-io/mediacms/commit/d9b1d6cab1d2bdfc16f799a0a27b64313e2e0d22))
* semantic release ([b76282f](https://github.com/mediacms-io/mediacms/commit/b76282f9e465a39c2da5e9a22184d1db23de3f56))
### Bug Fixes
* add delay to task creation ([1b3cdfd](https://github.com/mediacms-io/mediacms/commit/1b3cdfd302abc5e69ebe01ca52b5091f3b24c0b2))
* Add regex denoter and improve celerybeat gitignore ([#1446](https://github.com/mediacms-io/mediacms/issues/1446)) ([90331f3](https://github.com/mediacms-io/mediacms/commit/90331f3b4a2a5737de9dd75ab45c096944813c42))
* adjust poster url for audio ([01912ea](https://github.com/mediacms-io/mediacms/commit/01912ea1f99ef43793a65712539d6264f1f6410f))
* Chapter numbering and preserve custom titles on segment reorder ([#1435](https://github.com/mediacms-io/mediacms/issues/1435)) ([cd7dd4f](https://github.com/mediacms-io/mediacms/commit/cd7dd4f72c9f0bac466c680f686a9ecfdd3a38dd))
* Show default chapter names in textarea instead of placeholder text ([#1428](https://github.com/mediacms-io/mediacms/issues/1428)) ([5eb6faf](https://github.com/mediacms-io/mediacms/commit/5eb6fafb8c6928b8bc3fe5f0c7af315273f78a55))
* static files ([#1429](https://github.com/mediacms-io/mediacms/issues/1429)) ([ba2c31b](https://github.com/mediacms-io/mediacms/commit/ba2c31b1e65b7f508dee598b1f2d86f01f9bf036))
### Documentation
* update page link ([aeef828](https://github.com/mediacms-io/mediacms/commit/aeef8284bfba2a9a7f69c684f96c54f0e0e0cf92))

View File

@@ -1 +1 @@
VERSION = "8.10" VERSION = "7.6"

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en" style="height: 100%">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Embedded Video - Full Screen</title>
<style>
body {
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
overflow: hidden;
}
</style>
</head>
<body>
<iframe
src="https://demo.mediacms.io/embed?m=zK2nirNLC"
style="
width: 100%;
max-width: calc(100vh * 16 / 9);
aspect-ratio: 16 / 9;
display: block;
margin: auto;
border: 0;
"
allowfullscreen
></iframe>
</body>
</html>

View File

@@ -204,6 +204,54 @@ class SeekIndicator extends Component {
</div> </div>
`; `;
textEl.textContent = 'Pause'; textEl.textContent = 'Pause';
} else if (direction === 'copy-url') {
iconEl.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;">
<div style="
width: ${circleSize};
height: ${circleSize};
border-radius: 50%;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
border: none;
outline: none;
box-sizing: border-box;
overflow: hidden;
">
<svg viewBox="0 0 24 24" width="${iconSize}" height="${iconSize}" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
</div>
</div>
`;
textEl.textContent = '';
} else if (direction === 'copy-embed') {
iconEl.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;">
<div style="
width: ${circleSize};
height: ${circleSize};
border-radius: 50%;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
border: none;
outline: none;
box-sizing: border-box;
overflow: hidden;
">
<svg viewBox="0 0 24 24" width="${iconSize}" height="${iconSize}" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));">
<path d="M16 18l6-6-6-6"/>
<path d="M8 6l-6 6 6 6"/>
</svg>
</div>
</div>
`;
textEl.textContent = '';
} }
// Clear any text content in the text element // Clear any text content in the text element
@@ -239,6 +287,11 @@ class SeekIndicator extends Component {
this.showTimeout = setTimeout(() => { this.showTimeout = setTimeout(() => {
this.hide(); this.hide();
}, 500); }, 500);
} else if (direction === 'copy-url' || direction === 'copy-embed') {
// Copy operations: 500ms (same as play/pause)
this.showTimeout = setTimeout(() => {
this.hide();
}, 500);
} }
} }

View File

@@ -14,10 +14,22 @@ class EmbedInfoOverlay extends Component {
this.authorThumbnail = options.authorThumbnail || ''; this.authorThumbnail = options.authorThumbnail || '';
this.videoTitle = options.videoTitle || 'Video'; this.videoTitle = options.videoTitle || 'Video';
this.videoUrl = options.videoUrl || ''; this.videoUrl = options.videoUrl || '';
this.showTitle = options.showTitle !== undefined ? options.showTitle : true;
this.showRelated = options.showRelated !== undefined ? options.showRelated : true;
this.showUserAvatar = options.showUserAvatar !== undefined ? options.showUserAvatar : true;
this.linkTitle = options.linkTitle !== undefined ? options.linkTitle : true;
// Initialize after player is ready // Initialize after player is ready
this.player().ready(() => { this.player().ready(() => {
this.createOverlay(); if (this.showTitle) {
this.createOverlay();
} else {
// Hide overlay element if showTitle is false
const overlay = this.el();
overlay.style.display = 'none';
overlay.style.opacity = '0';
overlay.style.visibility = 'hidden';
}
}); });
} }
@@ -49,7 +61,7 @@ class EmbedInfoOverlay extends Component {
`; `;
// Create avatar container // Create avatar container
if (this.authorThumbnail) { if (this.authorThumbnail && this.showUserAvatar) {
const avatarContainer = document.createElement('div'); const avatarContainer = document.createElement('div');
avatarContainer.className = 'embed-avatar-container'; avatarContainer.className = 'embed-avatar-container';
avatarContainer.style.cssText = ` avatarContainer.style.cssText = `
@@ -125,7 +137,7 @@ class EmbedInfoOverlay extends Component {
overflow: hidden; overflow: hidden;
`; `;
if (this.videoUrl) { if (this.videoUrl && this.linkTitle) {
const titleLink = document.createElement('a'); const titleLink = document.createElement('a');
titleLink.href = this.videoUrl; titleLink.href = this.videoUrl;
titleLink.target = '_blank'; titleLink.target = '_blank';
@@ -186,10 +198,16 @@ class EmbedInfoOverlay extends Component {
const player = this.player(); const player = this.player();
const overlay = this.el(); const overlay = this.el();
// If showTitle is false, ensure overlay is hidden
if (!this.showTitle) {
overlay.style.display = 'none';
overlay.style.opacity = '0';
overlay.style.visibility = 'hidden';
return;
}
// Sync overlay visibility with control bar visibility // Sync overlay visibility with control bar visibility
const updateOverlayVisibility = () => { const updateOverlayVisibility = () => {
const controlBar = player.getChild('controlBar');
if (!player.hasStarted()) { if (!player.hasStarted()) {
// Show overlay when video hasn't started (poster is showing) - like before // Show overlay when video hasn't started (poster is showing) - like before
overlay.style.opacity = '1'; overlay.style.opacity = '1';

View File

@@ -0,0 +1,47 @@
.video-context-menu {
position: fixed;
background-color: #282828;
border-radius: 4px;
padding: 4px 0;
min-width: 240px;
z-index: 10000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
.video-context-menu-item {
display: flex;
align-items: center;
padding: 10px 16px;
color: #ffffff;
cursor: pointer;
transition: background-color 0.15s ease;
font-size: 14px;
user-select: none;
}
.video-context-menu-item:hover {
background-color: #3d3d3d;
}
.video-context-menu-item:active {
background-color: #4a4a4a;
}
.video-context-menu-icon {
width: 18px;
height: 18px;
margin-right: 12px;
flex-shrink: 0;
stroke: currentColor;
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.video-context-menu-item span {
flex: 1;
white-space: nowrap;
}

View File

@@ -0,0 +1,85 @@
import React, { useEffect, useRef } from 'react';
import './VideoContextMenu.css';
function VideoContextMenu({ visible, position, onClose, onCopyVideoUrl, onCopyVideoUrlAtTime, onCopyEmbedCode }) {
const menuRef = useRef(null);
useEffect(() => {
if (visible && menuRef.current) {
// Position the menu
menuRef.current.style.left = `${position.x}px`;
menuRef.current.style.top = `${position.y}px`;
// Adjust if menu goes off screen
const rect = menuRef.current.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (rect.right > windowWidth) {
menuRef.current.style.left = `${position.x - rect.width}px`;
}
if (rect.bottom > windowHeight) {
menuRef.current.style.top = `${position.y - rect.height}px`;
}
}
}, [visible, position]);
useEffect(() => {
const handleClickOutside = (e) => {
if (visible && menuRef.current && !menuRef.current.contains(e.target)) {
onClose();
}
};
const handleEscape = (e) => {
if (e.key === 'Escape' && visible) {
onClose();
}
};
if (visible) {
// Use capture phase to catch events earlier, before they can be stopped
// Listen to both mousedown and click to ensure we catch all clicks
document.addEventListener('mousedown', handleClickOutside, true);
document.addEventListener('click', handleClickOutside, true);
document.addEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside, true);
document.removeEventListener('click', handleClickOutside, true);
document.removeEventListener('keydown', handleEscape);
};
}, [visible, onClose]);
if (!visible) return null;
return (
<div ref={menuRef} className="video-context-menu" onClick={(e) => e.stopPropagation()}>
<div className="video-context-menu-item" onClick={onCopyVideoUrl}>
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span>Copy video URL</span>
</div>
<div className="video-context-menu-item" onClick={onCopyVideoUrlAtTime}>
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span>Copy video URL at current time</span>
</div>
<div className="video-context-menu-item" onClick={onCopyEmbedCode}>
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 18l6-6-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 6l-6 6 6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span>Copy embed code</span>
</div>
</div>
);
}
export default VideoContextMenu;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useMemo } from 'react'; import React, { useEffect, useRef, useMemo, useState, useCallback } from 'react';
import videojs from 'video.js'; import videojs from 'video.js';
import 'video.js/dist/video-js.css'; import 'video.js/dist/video-js.css';
import '../../styles/embed.css'; import '../../styles/embed.css';
@@ -17,6 +17,7 @@ import CustomRemainingTime from '../controls/CustomRemainingTime';
import CustomChaptersOverlay from '../controls/CustomChaptersOverlay'; import CustomChaptersOverlay from '../controls/CustomChaptersOverlay';
import CustomSettingsMenu from '../controls/CustomSettingsMenu'; import CustomSettingsMenu from '../controls/CustomSettingsMenu';
import SeekIndicator from '../controls/SeekIndicator'; import SeekIndicator from '../controls/SeekIndicator';
import VideoContextMenu from '../overlays/VideoContextMenu';
import UserPreferences from '../../utils/UserPreferences'; import UserPreferences from '../../utils/UserPreferences';
import PlayerConfig from '../../config/playerConfig'; import PlayerConfig from '../../config/playerConfig';
import { AutoplayHandler } from '../../utils/AutoplayHandler'; import { AutoplayHandler } from '../../utils/AutoplayHandler';
@@ -169,7 +170,7 @@ const enableStandardButtonTooltips = (player) => {
}, 500); // Delay to ensure all components are ready }, 500); // Delay to ensure all components are ready
}; };
function VideoJSPlayer({ videoId = 'default-video' }) { function VideoJSPlayer({ videoId = 'default-video', showTitle = true, showRelated = true, showUserAvatar = true, linkTitle = true, urlTimestamp = null }) {
const videoRef = useRef(null); const videoRef = useRef(null);
const playerRef = useRef(null); // Track the player instance const playerRef = useRef(null); // Track the player instance
const userPreferences = useRef(new UserPreferences()); // User preferences instance const userPreferences = useRef(new UserPreferences()); // User preferences instance
@@ -177,25 +178,17 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
const keyboardHandler = useRef(null); // Keyboard handler instance const keyboardHandler = useRef(null); // Keyboard handler instance
const playbackEventHandler = useRef(null); // Playback event handler instance const playbackEventHandler = useRef(null); // Playback event handler instance
// Context menu state
const [contextMenuVisible, setContextMenuVisible] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
// Check if this is an embed player (disable next video and autoplay features) // Check if this is an embed player (disable next video and autoplay features)
const isEmbedPlayer = videoId === 'video-embed'; const isEmbedPlayer = videoId === 'video-embed';
// Utility function to detect touch devices
const isTouchDevice = useMemo(() => {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
}, []);
// Utility function to detect iOS devices
const isIOS = useMemo(() => {
return (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
);
}, []);
// Environment-based development mode configuration // Environment-based development mode configuration
const isDevMode = import.meta.env.VITE_DEV_MODE === 'true' || window.location.hostname.includes('vercel.app'); const isDevMode = import.meta.env.VITE_DEV_MODE === 'true' || window.location.hostname.includes('vercel.app');
// Safely access window.MEDIA_DATA with fallback using useMemo
// Read options from window.MEDIA_DATA if available (for consistency with embed logic)
const mediaData = useMemo( const mediaData = useMemo(
() => () =>
typeof window !== 'undefined' && window.MEDIA_DATA typeof window !== 'undefined' && window.MEDIA_DATA
@@ -214,12 +207,37 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
}, },
siteUrl: 'https://deic.mediacms.io', siteUrl: 'https://deic.mediacms.io',
nextLink: 'https://deic.mediacms.io/view?m=elygiagorgechania', nextLink: 'https://deic.mediacms.io/view?m=elygiagorgechania',
urlAutoplay: true,
urlMuted: false,
}, },
[] []
); );
// Helper to get effective value (prop or MEDIA_DATA or default)
const getOption = (propKey, mediaDataKey, defaultValue) => {
if (isEmbedPlayer) {
if (mediaData[mediaDataKey] !== undefined) return mediaData[mediaDataKey];
}
return propKey !== undefined ? propKey : defaultValue;
};
const finalShowTitle = getOption(showTitle, 'showTitle', true);
const finalShowRelated = getOption(showRelated, 'showRelated', true);
const finalShowUserAvatar = getOption(showUserAvatar, 'showUserAvatar', true);
const finalLinkTitle = getOption(linkTitle, 'linkTitle', true);
const finalTimestamp = getOption(urlTimestamp, 'urlTimestamp', null);
// Utility function to detect touch devices
const isTouchDevice = useMemo(() => {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
}, []);
// Utility function to detect iOS devices
const isIOS = useMemo(() => {
return (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
);
}, []);
// Define chapters as JSON object // Define chapters as JSON object
// Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON // Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON
// CONDITIONAL LOGIC: // CONDITIONAL LOGIC:
@@ -531,8 +549,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
isPlayList: mediaData?.isPlayList, isPlayList: mediaData?.isPlayList,
related_media: mediaData.data?.related_media || [], related_media: mediaData.data?.related_media || [],
nextLink: mediaData?.nextLink || null, nextLink: mediaData?.nextLink || null,
urlAutoplay: mediaData?.urlAutoplay || true,
urlMuted: mediaData?.urlMuted || false,
sources: getVideoSources(), sources: getVideoSources(),
}; };
@@ -738,6 +754,212 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
} }
}; };
// Context menu handlers
const handleContextMenu = useCallback((e) => {
// Only handle if clicking on video player area
const target = e.target;
const isVideoPlayerArea =
target.closest('.video-js') ||
target.classList.contains('vjs-tech') ||
target.tagName === 'VIDEO' ||
target.closest('video');
if (isVideoPlayerArea) {
e.preventDefault();
e.stopPropagation();
setContextMenuPosition({ x: e.clientX, y: e.clientY });
setContextMenuVisible(true);
}
}, []);
const closeContextMenu = () => {
setContextMenuVisible(false);
};
// Helper function to get media ID
const getMediaId = () => {
if (typeof window !== 'undefined' && window.MEDIA_DATA?.data?.friendly_token) {
return window.MEDIA_DATA.data.friendly_token;
}
if (mediaData?.data?.friendly_token) {
return mediaData.data.friendly_token;
}
// Try to get from URL (works for both main page and embed page)
if (typeof window !== 'undefined') {
const urlParams = new URLSearchParams(window.location.search);
const mediaIdFromUrl = urlParams.get('m');
if (mediaIdFromUrl) {
return mediaIdFromUrl;
}
// Also check if we're on an embed page with media ID in path
const pathMatch = window.location.pathname.match(/\/embed\/([^/?]+)/);
if (pathMatch) {
return pathMatch[1];
}
}
return currentVideo.id || 'default-video';
};
// Helper function to get base origin URL (handles embed mode)
const getBaseOrigin = () => {
if (typeof window !== 'undefined') {
// In embed mode, try to get origin from parent window if possible
// Otherwise use current window origin
try {
// Check if we're in an iframe and can access parent
if (window.parent !== window && window.parent.location.origin) {
return window.parent.location.origin;
}
} catch {
// Cross-origin iframe, use current origin
}
return window.location.origin;
}
return mediaData.siteUrl || 'https://deic.mediacms.io';
};
// Helper function to get embed URL
const getEmbedUrl = () => {
const mediaId = getMediaId();
const origin = getBaseOrigin();
// Try to get embed URL from config or construct it
if (typeof window !== 'undefined' && window.MediaCMS?.config?.url?.embed) {
return window.MediaCMS.config.url.embed + mediaId;
}
// Fallback: construct embed URL (check if current URL is embed format)
if (typeof window !== 'undefined' && window.location.pathname.includes('/embed')) {
// If we're already on an embed page, use current URL format
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('m', mediaId);
return currentUrl.toString();
}
// Default embed URL format
return `${origin}/embed?m=${mediaId}`;
};
// Copy video URL to clipboard
const handleCopyVideoUrl = async () => {
const mediaId = getMediaId();
const origin = getBaseOrigin();
const videoUrl = `${origin}/view?m=${mediaId}`;
// Show copy icon
if (customComponents.current?.seekIndicator) {
customComponents.current.seekIndicator.show('copy-url');
}
try {
await navigator.clipboard.writeText(videoUrl);
closeContextMenu();
// You can add a notification here if needed
} catch (err) {
console.error('Failed to copy video URL:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = videoUrl;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
closeContextMenu();
}
};
// Copy video URL at current time to clipboard
const handleCopyVideoUrlAtTime = async () => {
if (!playerRef.current) {
closeContextMenu();
return;
}
const currentTime = Math.floor(playerRef.current.currentTime() || 0);
const mediaId = getMediaId();
const origin = getBaseOrigin();
const videoUrl = `${origin}/view?m=${mediaId}&t=${currentTime}`;
// Show copy icon
if (customComponents.current?.seekIndicator) {
customComponents.current.seekIndicator.show('copy-url');
}
try {
await navigator.clipboard.writeText(videoUrl);
closeContextMenu();
} catch (err) {
console.error('Failed to copy video URL at time:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = videoUrl;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
closeContextMenu();
}
};
// Copy embed code to clipboard
const handleCopyEmbedCode = async () => {
const embedUrl = getEmbedUrl();
const embedCode = `<iframe width="560" height="315" src="${embedUrl}" frameborder="0" allowfullscreen></iframe>`;
// Show copy embed icon
if (customComponents.current?.seekIndicator) {
customComponents.current.seekIndicator.show('copy-embed');
}
try {
await navigator.clipboard.writeText(embedCode);
closeContextMenu();
} catch (err) {
console.error('Failed to copy embed code:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = embedCode;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
closeContextMenu();
}
};
// Add context menu handler directly to video element and document (works before and after Video.js initialization)
useEffect(() => {
const videoElement = videoRef.current;
// Attach to document with capture to catch all contextmenu events, then filter
const documentHandler = (e) => {
// Check if the event originated from within the video player
const target = e.target;
const playerWrapper =
videoElement?.closest('.video-js') || document.querySelector(`#${videoId}`)?.closest('.video-js');
if (playerWrapper && (playerWrapper.contains(target) || target === playerWrapper)) {
handleContextMenu(e);
}
};
// Use capture phase on document to catch before anything else
document.addEventListener('contextmenu', documentHandler, true);
// Also attach directly to video element
if (videoElement) {
videoElement.addEventListener('contextmenu', handleContextMenu, true);
}
return () => {
document.removeEventListener('contextmenu', documentHandler, true);
if (videoElement) {
videoElement.removeEventListener('contextmenu', handleContextMenu, true);
}
};
}, [handleContextMenu, videoId]);
useEffect(() => { useEffect(() => {
// Only initialize if we don't already have a player and element exists // Only initialize if we don't already have a player and element exists
if (videoRef.current && !playerRef.current) { if (videoRef.current && !playerRef.current) {
@@ -1078,6 +1300,9 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
currentVideo, currentVideo,
relatedVideos, relatedVideos,
goToNextVideo, goToNextVideo,
showRelated: finalShowRelated,
showUserAvatar: finalShowUserAvatar,
linkTitle: finalLinkTitle,
}); });
customComponents.current.endScreenHandler = endScreenHandler; // Store for cleanup customComponents.current.endScreenHandler = endScreenHandler; // Store for cleanup
@@ -1098,8 +1323,8 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
} }
// Handle URL timestamp parameter // Handle URL timestamp parameter
if (mediaData.urlTimestamp !== null && mediaData.urlTimestamp >= 0) { if (finalTimestamp !== null && finalTimestamp >= 0) {
const timestamp = mediaData.urlTimestamp; const timestamp = finalTimestamp;
// Wait for video metadata to be loaded before seeking // Wait for video metadata to be loaded before seeking
if (playerRef.current.readyState() >= 1) { if (playerRef.current.readyState() >= 1) {
@@ -1997,6 +2222,10 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
authorThumbnail: currentVideo.author_thumbnail, authorThumbnail: currentVideo.author_thumbnail,
videoTitle: currentVideo.title, videoTitle: currentVideo.title,
videoUrl: currentVideo.url, videoUrl: currentVideo.url,
showTitle: finalShowTitle,
showRelated: finalShowRelated,
showUserAvatar: finalShowUserAvatar,
linkTitle: finalLinkTitle,
}); });
} }
// END: Add Embed Info Overlay Component // END: Add Embed Info Overlay Component
@@ -2083,52 +2312,113 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
// Make the video element focusable // Make the video element focusable
const videoElement = playerRef.current.el(); const videoElement = playerRef.current.el();
videoElement.setAttribute('tabindex', '0'); videoElement.setAttribute('tabindex', '0');
videoElement.focus();
if (!isEmbedPlayer) {
videoElement.focus();
}
// Add context menu (right-click) handler to the player wrapper and video element
// Attach to player wrapper (this catches all clicks on the player)
videoElement.addEventListener('contextmenu', handleContextMenu, true);
// Also try to attach to the actual video tech element
const attachContextMenu = () => {
const techElement =
playerRef.current.el().querySelector('.vjs-tech') ||
playerRef.current.el().querySelector('video') ||
(playerRef.current.tech() && playerRef.current.tech().el());
if (techElement && techElement !== videoRef.current && techElement !== videoElement) {
// Use capture phase to catch before Video.js might prevent it
techElement.addEventListener('contextmenu', handleContextMenu, true);
return true;
}
return false;
};
// Try to attach immediately
attachContextMenu();
// Also try after a short delay in case elements aren't ready yet
setTimeout(() => {
attachContextMenu();
}, 100);
// Also try when video is loaded
playerRef.current.one('loadedmetadata', () => {
attachContextMenu();
});
} }
}); });
} }
//}, 0); //}, 0);
} }
// Cleanup: Remove context menu event listener
return () => {
if (playerRef.current && playerRef.current.el()) {
const playerEl = playerRef.current.el();
playerEl.removeEventListener('contextmenu', handleContextMenu, true);
const techElement =
playerEl.querySelector('.vjs-tech') ||
playerEl.querySelector('video') ||
(playerRef.current.tech() && playerRef.current.tech().el());
if (techElement) {
techElement.removeEventListener('contextmenu', handleContextMenu, true);
}
}
};
}, []); }, []);
return ( return (
<video <>
ref={videoRef} <video
id={videoId} ref={videoRef}
controls={true} id={videoId}
className={`video-js vjs-fluid vjs-default-skin${currentVideo.useRoundedCorners ? ' video-js-rounded-corners' : ''}`} controls={true}
preload="auto" className={`video-js ${isEmbedPlayer ? 'vjs-fill' : 'vjs-fluid'} vjs-default-skin${currentVideo.useRoundedCorners ? ' video-js-rounded-corners' : ''}`}
poster={currentVideo.poster} preload="auto"
tabIndex="0" poster={currentVideo.poster}
> tabIndex="0"
{/* <source src="/videos/sample-video.mp4" type="video/mp4" /> >
<source src="/videos/sample-video.webm" type="video/webm" /> */} {/* <source src="/videos/sample-video.mp4" type="video/mp4" />
<p className="vjs-no-js"> <source src="/videos/sample-video.webm" type="video/webm" /> */}
To view this video please enable JavaScript, and consider upgrading to a web browser that <p className="vjs-no-js">
<a href="https://videojs.com/html5-video-support/" target="_blank"> To view this video please enable JavaScript, and consider upgrading to a web browser that
supports HTML5 video <a href="https://videojs.com/html5-video-support/" target="_blank">
</a> supports HTML5 video
</p> </a>
</p>
{/* Add subtitle tracks */} {/* Add subtitle tracks */}
{/* {subtitleTracks && {/* {subtitleTracks &&
subtitleTracks.map((track, index) => ( subtitleTracks.map((track, index) => (
<track <track
key={index} key={index}
kind={track.kind} kind={track.kind}
src={track.src} src={track.src}
srcLang={track.srclang} srcLang={track.srclang}
label={track.label} label={track.label}
default={track.default} default={track.default}
/> />
))} */} ))} */}
{/* {/*
<track kind="chapters" src="/sample-chapters.vtt" /> */} <track kind="chapters" src="/sample-chapters.vtt" /> */}
{/* Add chapters track */} {/* Add chapters track */}
{/* {chaptersData && {/* {chaptersData &&
chaptersData.length > 0 && chaptersData.length > 0 &&
(console.log('chaptersData', chaptersData), (<track kind="chapters" src="/sample-chapters.vtt" />))} */} (console.log('chaptersData', chaptersData), (<track kind="chapters" src="/sample-chapters.vtt" />))} */}
</video> </video>
<VideoContextMenu
visible={contextMenuVisible}
position={contextMenuPosition}
onClose={closeContextMenu}
onCopyVideoUrl={handleCopyVideoUrl}
onCopyVideoUrlAtTime={handleCopyVideoUrlAtTime}
onCopyEmbedCode={handleCopyEmbedCode}
/>
</>
); );
} }

View File

@@ -63,7 +63,17 @@ export class EndScreenHandler {
} }
handleVideoEnded() { handleVideoEnded() {
const { isEmbedPlayer, userPreferences, mediaData, currentVideo, relatedVideos, goToNextVideo } = this.options; const {
isEmbedPlayer,
userPreferences,
mediaData,
currentVideo,
relatedVideos,
goToNextVideo,
showRelated,
showUserAvatar,
linkTitle,
} = this.options;
// For embed players, show big play button when video ends // For embed players, show big play button when video ends
if (isEmbedPlayer) { if (isEmbedPlayer) {
@@ -73,6 +83,34 @@ export class EndScreenHandler {
} }
} }
// If showRelated is false, we don't show the end screen or autoplay countdown
if (showRelated === false) {
// But we still want to keep the control bar visible and hide the poster
setTimeout(() => {
if (this.player && !this.player.isDisposed()) {
const playerEl = this.player.el();
if (playerEl) {
// Hide poster elements
const posterElements = playerEl.querySelectorAll('.vjs-poster');
posterElements.forEach((posterEl) => {
posterEl.style.display = 'none';
posterEl.style.visibility = 'hidden';
posterEl.style.opacity = '0';
});
// Keep control bar visible
const controlBar = this.player.getChild('controlBar');
if (controlBar) {
controlBar.show();
controlBar.el().style.opacity = '1';
controlBar.el().style.pointerEvents = 'auto';
}
}
}
}, 50);
return;
}
// Keep controls active after video ends // Keep controls active after video ends
setTimeout(() => { setTimeout(() => {
if (this.player && !this.player.isDisposed()) { if (this.player && !this.player.isDisposed()) {

View File

@@ -31,8 +31,11 @@ const VideoJSEmbed = ({
poster, poster,
previewSprite, previewSprite,
subtitlesInfo, subtitlesInfo,
enableAutoplay,
inEmbed, inEmbed,
showTitle,
showRelated,
showUserAvatar,
linkTitle,
hasTheaterMode, hasTheaterMode,
hasNextLink, hasNextLink,
nextLink, nextLink,
@@ -62,8 +65,10 @@ const VideoJSEmbed = ({
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// Get URL parameters for autoplay, muted, and timestamp // Get URL parameters for autoplay, muted, and timestamp
const urlTimestamp = getUrlParameter('t'); const urlTimestamp = getUrlParameter('t');
const urlAutoplay = getUrlParameter('autoplay');
const urlMuted = getUrlParameter('muted'); const urlMuted = getUrlParameter('muted');
const urlShowRelated = getUrlParameter('showRelated');
const urlShowUserAvatar = getUrlParameter('showUserAvatar');
const urlLinkTitle = getUrlParameter('linkTitle');
window.MEDIA_DATA = { window.MEDIA_DATA = {
data: data || {}, data: data || {},
@@ -71,7 +76,7 @@ const VideoJSEmbed = ({
version: version, version: version,
isPlayList: isPlayList, isPlayList: isPlayList,
playerVolume: playerVolume || 0.5, playerVolume: playerVolume || 0.5,
playerSoundMuted: playerSoundMuted || (urlMuted === '1'), playerSoundMuted: urlMuted === '1',
videoQuality: videoQuality || 'auto', videoQuality: videoQuality || 'auto',
videoPlaybackSpeed: videoPlaybackSpeed || 1, videoPlaybackSpeed: videoPlaybackSpeed || 1,
inTheaterMode: inTheaterMode || false, inTheaterMode: inTheaterMode || false,
@@ -83,8 +88,11 @@ const VideoJSEmbed = ({
poster: poster || '', poster: poster || '',
previewSprite: previewSprite || null, previewSprite: previewSprite || null,
subtitlesInfo: subtitlesInfo || [], subtitlesInfo: subtitlesInfo || [],
enableAutoplay: enableAutoplay || (urlAutoplay === '1'),
inEmbed: inEmbed || false, inEmbed: inEmbed || false,
showTitle: showTitle || false,
showRelated: showRelated !== undefined ? showRelated : (urlShowRelated === '1' || urlShowRelated === 'true' || urlShowRelated === null),
showUserAvatar: showUserAvatar !== undefined ? showUserAvatar : (urlShowUserAvatar === '1' || urlShowUserAvatar === 'true' || urlShowUserAvatar === null),
linkTitle: linkTitle !== undefined ? linkTitle : (urlLinkTitle === '1' || urlLinkTitle === 'true' || urlLinkTitle === null),
hasTheaterMode: hasTheaterMode || false, hasTheaterMode: hasTheaterMode || false,
hasNextLink: hasNextLink || false, hasNextLink: hasNextLink || false,
nextLink: nextLink || null, nextLink: nextLink || null,
@@ -92,8 +100,10 @@ const VideoJSEmbed = ({
errorMessage: errorMessage || '', errorMessage: errorMessage || '',
// URL parameters // URL parameters
urlTimestamp: urlTimestamp ? parseInt(urlTimestamp, 10) : null, urlTimestamp: urlTimestamp ? parseInt(urlTimestamp, 10) : null,
urlAutoplay: urlAutoplay === '1',
urlMuted: urlMuted === '1', urlMuted: urlMuted === '1',
urlShowRelated: urlShowRelated === '1' || urlShowRelated === 'true',
urlShowUserAvatar: urlShowUserAvatar === '1' || urlShowUserAvatar === 'true',
urlLinkTitle: urlLinkTitle === '1' || urlLinkTitle === 'true',
onClickNextCallback: onClickNextCallback || null, onClickNextCallback: onClickNextCallback || null,
onClickPreviousCallback: onClickPreviousCallback || null, onClickPreviousCallback: onClickPreviousCallback || null,
onStateUpdateCallback: onStateUpdateCallback || null, onStateUpdateCallback: onStateUpdateCallback || null,
@@ -176,11 +186,17 @@ const VideoJSEmbed = ({
// Scroll to the video player with smooth behavior // Scroll to the video player with smooth behavior
const videoElement = document.querySelector(inEmbedRef.current ? '#video-embed' : '#video-main'); const videoElement = document.querySelector(inEmbedRef.current ? '#video-embed' : '#video-main');
if (videoElement) { if (videoElement) {
videoElement.scrollIntoView({ const urlScroll = getUrlParameter('scroll');
behavior: 'smooth', const isIframe = window.parent !== window;
block: 'center',
inline: 'nearest' // Only scroll if not in an iframe, OR if explicitly requested via scroll=1 parameter
}); if (!isIframe || urlScroll === '1' || urlScroll === 'true') {
videoElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}
} }
} else { } else {
console.warn('VideoJS player not found for timestamp navigation'); console.warn('VideoJS player not found for timestamp navigation');
@@ -220,7 +236,14 @@ const VideoJSEmbed = ({
return ( return (
<div className="video-js-wrapper" ref={containerRef}> <div className="video-js-wrapper" ref={containerRef}>
{inEmbed ? <div id="video-js-root-embed" className="video-js-root-embed" /> : <div id="video-js-root-main" className="video-js-root-main" />} {inEmbed ? (
<div
id="video-js-root-embed"
className="video-js-root-embed"
/>
) : (
<div id="video-js-root-main" className="video-js-root-main" />
)}
</div> </div>
); );
}; };

View File

@@ -4,10 +4,32 @@ import { LinksContext, SiteConsumer } from '../../utils/contexts/';
import { PageStore, MediaPageStore } from '../../utils/stores/'; import { PageStore, MediaPageStore } from '../../utils/stores/';
import { PageActions, MediaPageActions } from '../../utils/actions/'; import { PageActions, MediaPageActions } from '../../utils/actions/';
import { CircleIconButton, MaterialIcon, NumericInputWithUnit } from '../_shared/'; import { CircleIconButton, MaterialIcon, NumericInputWithUnit } from '../_shared/';
import VideoViewer from '../media-viewer/VideoViewer';
const EMBED_OPTIONS_STORAGE_KEY = 'mediacms_embed_options';
function loadEmbedOptions() {
try {
const saved = localStorage.getItem(EMBED_OPTIONS_STORAGE_KEY);
if (saved) {
return JSON.parse(saved);
}
} catch (e) {
// Ignore localStorage errors
}
return null;
}
function saveEmbedOptions(options) {
try {
localStorage.setItem(EMBED_OPTIONS_STORAGE_KEY, JSON.stringify(options));
} catch (e) {
// Ignore localStorage errors
}
}
export function MediaShareEmbed(props) { export function MediaShareEmbed(props) {
const embedVideoDimensions = PageStore.get('config-options').embedded.video.dimensions; const embedVideoDimensions = PageStore.get('config-options').embedded.video.dimensions;
const savedOptions = loadEmbedOptions();
const links = useContext(LinksContext); const links = useContext(LinksContext);
@@ -18,12 +40,19 @@ export function MediaShareEmbed(props) {
const onRightBottomRef = useRef(null); const onRightBottomRef = useRef(null);
const [maxHeight, setMaxHeight] = useState(window.innerHeight - 144 + 56); const [maxHeight, setMaxHeight] = useState(window.innerHeight - 144 + 56);
const [keepAspectRatio, setKeepAspectRatio] = useState(false); const [keepAspectRatio, setKeepAspectRatio] = useState(savedOptions?.keepAspectRatio ?? true);
const [aspectRatio, setAspectRatio] = useState('16:9'); const [showTitle, setShowTitle] = useState(savedOptions?.showTitle ?? true);
const [embedWidthValue, setEmbedWidthValue] = useState(embedVideoDimensions.width); const [showRelated, setShowRelated] = useState(savedOptions?.showRelated ?? true);
const [embedWidthUnit, setEmbedWidthUnit] = useState(embedVideoDimensions.widthUnit); const [showUserAvatar, setShowUserAvatar] = useState(savedOptions?.showUserAvatar ?? true);
const [embedHeightValue, setEmbedHeightValue] = useState(embedVideoDimensions.height); const [linkTitle, setLinkTitle] = useState(savedOptions?.linkTitle ?? true);
const [embedHeightUnit, setEmbedHeightUnit] = useState(embedVideoDimensions.heightUnit); const [responsive, setResponsive] = useState(savedOptions?.responsive ?? false);
const [startAt, setStartAt] = useState(false);
const [startTime, setStartTime] = useState('0:00');
const [aspectRatio, setAspectRatio] = useState(savedOptions?.aspectRatio ?? '16:9');
const [embedWidthValue, setEmbedWidthValue] = useState(savedOptions?.embedWidthValue ?? embedVideoDimensions.width);
const [embedWidthUnit, setEmbedWidthUnit] = useState(savedOptions?.embedWidthUnit ?? embedVideoDimensions.widthUnit);
const [embedHeightValue, setEmbedHeightValue] = useState(savedOptions?.embedHeightValue ?? embedVideoDimensions.height);
const [embedHeightUnit, setEmbedHeightUnit] = useState(savedOptions?.embedHeightUnit ?? embedVideoDimensions.heightUnit);
const [rightMiddlePositionTop, setRightMiddlePositionTop] = useState(60); const [rightMiddlePositionTop, setRightMiddlePositionTop] = useState(60);
const [rightMiddlePositionBottom, setRightMiddlePositionBottom] = useState(60); const [rightMiddlePositionBottom, setRightMiddlePositionBottom] = useState(60);
const [unitOptions, setUnitOptions] = useState([ const [unitOptions, setUnitOptions] = useState([
@@ -71,36 +100,65 @@ export function MediaShareEmbed(props) {
setEmbedHeightUnit(newVal); setEmbedHeightUnit(newVal);
} }
function onKeepAspectRatioChange() { function onShowTitleChange() {
const newVal = !keepAspectRatio; setShowTitle(!showTitle);
}
const arr = aspectRatio.split(':'); function onShowRelatedChange() {
const x = arr[0]; setShowRelated(!showRelated);
const y = arr[1]; }
setKeepAspectRatio(newVal); function onShowUserAvatarChange() {
setEmbedWidthUnit(newVal ? 'px' : embedWidthUnit); setShowUserAvatar(!showUserAvatar);
setEmbedHeightUnit(newVal ? 'px' : embedHeightUnit); }
setEmbedHeightValue(newVal ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue);
setUnitOptions( function onLinkTitleChange() {
newVal setLinkTitle(!linkTitle);
? [{ key: 'px', label: 'px' }] }
: [
{ key: 'px', label: 'px' }, function onResponsiveChange() {
{ key: 'percent', label: '%' }, const nextResponsive = !responsive;
] setResponsive(nextResponsive);
);
if (!nextResponsive) {
if (aspectRatio !== 'custom') {
const arr = aspectRatio.split(':');
const x = arr[0];
const y = arr[1];
setKeepAspectRatio(true);
setEmbedHeightValue(parseInt((embedWidthValue * y) / x, 10));
} else {
setKeepAspectRatio(false);
}
} else {
setKeepAspectRatio(false);
}
}
function onStartAtChange() {
setStartAt(!startAt);
}
function onStartTimeChange(e) {
setStartTime(e.target.value);
} }
function onAspectRatioChange() { function onAspectRatioChange() {
const newVal = aspectRatioValueRef.current.value; const newVal = aspectRatioValueRef.current.value;
const arr = newVal.split(':'); if (newVal === 'custom') {
const x = arr[0]; setAspectRatio(newVal);
const y = arr[1]; setKeepAspectRatio(false);
} else {
const arr = newVal.split(':');
const x = arr[0];
const y = arr[1];
setAspectRatio(newVal); setAspectRatio(newVal);
setEmbedHeightValue(keepAspectRatio ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue); setKeepAspectRatio(true);
setEmbedHeightValue(parseInt((embedWidthValue * y) / x, 10));
}
} }
function onWindowResize() { function onWindowResize() {
@@ -130,13 +188,88 @@ export function MediaShareEmbed(props) {
}; };
}, []); }, []);
// Save embed options to localStorage when they change (except startAt/startTime)
useEffect(() => {
saveEmbedOptions({
showTitle,
showRelated,
showUserAvatar,
linkTitle,
responsive,
aspectRatio,
embedWidthValue,
embedWidthUnit,
embedHeightValue,
embedHeightUnit,
keepAspectRatio,
});
}, [showTitle, showRelated, showUserAvatar, linkTitle, responsive, aspectRatio, embedWidthValue, embedWidthUnit, embedHeightValue, embedHeightUnit, keepAspectRatio]);
function getEmbedCode() {
const mediaId = MediaPageStore.get('media-id');
const params = new URLSearchParams();
if (showTitle) params.set('showTitle', '1');
else params.set('showTitle', '0');
if (showRelated) params.set('showRelated', '1');
else params.set('showRelated', '0');
if (showUserAvatar) params.set('showUserAvatar', '1');
else params.set('showUserAvatar', '0');
if (linkTitle) params.set('linkTitle', '1');
else params.set('linkTitle', '0');
if (startAt && startTime) {
const parts = startTime.split(':').reverse();
let seconds = 0;
if (parts[0]) seconds += parseInt(parts[0], 10) || 0;
if (parts[1]) seconds += (parseInt(parts[1], 10) || 0) * 60;
if (parts[2]) seconds += (parseInt(parts[2], 10) || 0) * 3600;
if (seconds > 0) params.set('t', seconds);
}
const separator = links.embed.includes('?') ? '&' : '?';
const finalUrl = `${links.embed}${mediaId}${separator}${params.toString()}`;
if (responsive) {
if (aspectRatio === 'custom') {
// Use current width/height values to calculate aspect ratio for custom
const ratio = `${embedWidthValue} / ${embedHeightValue}`;
const maxWidth = `calc(100vh * ${embedWidthValue} / ${embedHeightValue})`;
return `<iframe src="${finalUrl}" style="width:100%;max-width:${maxWidth};aspect-ratio:${ratio};display:block;margin:auto;border:0;" allowFullScreen></iframe>`;
}
const arr = aspectRatio.split(':');
const ratio = `${arr[0]} / ${arr[1]}`;
const maxWidth = `calc(100vh * ${arr[0]} / ${arr[1]})`;
return `<iframe src="${finalUrl}" style="width:100%;max-width:${maxWidth};aspect-ratio:${ratio};display:block;margin:auto;border:0;" allowFullScreen></iframe>`;
}
const width = 'percent' === embedWidthUnit ? embedWidthValue + '%' : embedWidthValue;
const height = 'percent' === embedHeightUnit ? embedHeightValue + '%' : embedHeightValue;
return `<iframe width="${width}" height="${height}" src="${finalUrl}" frameBorder="0" allowFullScreen></iframe>`;
}
return ( return (
<div className="share-embed" style={{ maxHeight: maxHeight + 'px' }}> <div className="share-embed" style={{ maxHeight: maxHeight + 'px' }}>
<div className="share-embed-inner"> <div className="share-embed-inner">
<div className="on-left"> <div className="on-left">
<div className="media-embed-wrap"> <div className="media-embed-wrap">
<SiteConsumer> <SiteConsumer>
{(site) => <VideoViewer data={MediaPageStore.get('media-data')} siteUrl={site.url} inEmbed={true} />} {(site) => {
const previewUrl = `${links.embed + MediaPageStore.get('media-id')}&showTitle=${showTitle ? '1' : '0'}&showRelated=${showRelated ? '1' : '0'}&showUserAvatar=${showUserAvatar ? '1' : '0'}&linkTitle=${linkTitle ? '1' : '0'}${startAt ? '&t=' + (startTime.split(':').reverse().reduce((acc, cur, i) => acc + (parseInt(cur, 10) || 0) * Math.pow(60, i), 0)) : ''}`;
const style = {};
style.width = '100%';
style.height = '480px';
style.overflow = 'hidden';
return (
<div style={style}>
<iframe width="100%" height="100%" src={previewUrl} frameBorder="0" allowFullScreen></iframe>
</div>
);
}}
</SiteConsumer> </SiteConsumer>
</div> </div>
</div> </div>
@@ -158,16 +291,7 @@ export function MediaShareEmbed(props) {
> >
<textarea <textarea
readOnly readOnly
value={ value={getEmbedCode()}
'<iframe width="' +
('percent' === embedWidthUnit ? embedWidthValue + '%' : embedWidthValue) +
'" height="' +
('percent' === embedHeightUnit ? embedHeightValue + '%' : embedHeightValue) +
'" src="' +
links.embed +
MediaPageStore.get('media-id') +
'" frameborder="0" allowfullscreen></iframe>'
}
></textarea> ></textarea>
<div className="iframe-config"> <div className="iframe-config">
@@ -179,59 +303,106 @@ export function MediaShareEmbed(props) {
</div>*/} </div>*/}
<div className="option-content"> <div className="option-content">
<div className="ratio-options"> <div className="ratio-options" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 10px' }}>
<div className="options-group"> <div className="options-group">
<label style={{ minHeight: '36px' }}> <label style={{ minHeight: '36px', whiteSpace: 'nowrap' }}>
<input type="checkbox" checked={keepAspectRatio} onChange={onKeepAspectRatioChange} /> <input type="checkbox" checked={showTitle} onChange={onShowTitleChange} />
Keep aspect ratio Show title
</label> </label>
</div> </div>
{!keepAspectRatio ? null : ( <div className="options-group">
<div className="options-group"> <label style={{ minHeight: '36px', whiteSpace: 'nowrap', opacity: showTitle ? 1 : 0.5 }}>
<select ref={aspectRatioValueRef} onChange={onAspectRatioChange} value={aspectRatio}> <input type="checkbox" checked={linkTitle} onChange={onLinkTitleChange} disabled={!showTitle} />
<optgroup label="Horizontal orientation"> Link title
<option value="16:9">16:9</option> </label>
<option value="4:3">4:3</option> </div>
<option value="3:2">3:2</option>
</optgroup> <div className="options-group">
<optgroup label="Vertical orientation"> <label style={{ minHeight: '36px', whiteSpace: 'nowrap' }}>
<option value="9:16">9:16</option> <input type="checkbox" checked={showRelated} onChange={onShowRelatedChange} />
<option value="3:4">3:4</option> Show related
<option value="2:3">2:3</option> </label>
</optgroup> </div>
<div className="options-group">
<label style={{ minHeight: '36px', whiteSpace: 'nowrap', opacity: showTitle ? 1 : 0.5 }}>
<input type="checkbox" checked={showUserAvatar} onChange={onShowUserAvatarChange} disabled={!showTitle} />
Show user avatar
</label>
</div>
<div className="options-group" style={{ display: 'flex', alignItems: 'center' }}>
<label style={{ minHeight: '36px', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', marginRight: '10px' }}>
<input type="checkbox" checked={responsive} onChange={onResponsiveChange} />
Responsive
</label>
</div>
<div className="options-group" style={{ display: 'flex', alignItems: 'center' }}>
<label style={{ minHeight: '36px', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', marginRight: '10px' }}>
<input type="checkbox" checked={startAt} onChange={onStartAtChange} />
Start at
</label>
{startAt && (
<input
type="text"
value={startTime}
onChange={onStartTimeChange}
style={{ width: '60px', height: '28px', fontSize: '12px', padding: '2px 5px' }}
/>
)}
</div>
<div className="options-group" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<div style={{ fontSize: '12px', marginBottom: '4px', color: 'rgba(0,0,0,0.6)' }}>Aspect Ratio</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<select
ref={aspectRatioValueRef}
onChange={onAspectRatioChange}
value={aspectRatio}
style={{ height: '28px', fontSize: '12px' }}
>
<option value="16:9">16:9</option>
<option value="4:3">4:3</option>
<option value="3:2">3:2</option>
<option value="custom">Custom</option>
</select> </select>
</div> </div>
)} </div>
</div> </div>
<br /> <br />
<div className="options-group"> {!responsive && (
<NumericInputWithUnit <>
valueCallback={onEmbedWidthValueChange} <div className="options-group">
unitCallback={onEmbedWidthUnitChange} <NumericInputWithUnit
label={'Width'} valueCallback={onEmbedWidthValueChange}
defaultValue={parseInt(embedWidthValue, 10)} unitCallback={onEmbedWidthUnitChange}
defaultUnit={embedWidthUnit} label={'Width'}
minValue={1} defaultValue={parseInt(embedWidthValue, 10)}
maxValue={99999} defaultUnit={embedWidthUnit}
units={unitOptions} minValue={1}
/> maxValue={99999}
</div> units={unitOptions}
/>
</div>
<div className="options-group"> <div className="options-group">
<NumericInputWithUnit <NumericInputWithUnit
valueCallback={onEmbedHeightValueChange} valueCallback={onEmbedHeightValueChange}
unitCallback={onEmbedHeightUnitChange} unitCallback={onEmbedHeightUnitChange}
label={'Height'} label={'Height'}
defaultValue={parseInt(embedHeightValue, 10)} defaultValue={parseInt(embedHeightValue, 10)}
defaultUnit={embedHeightUnit} defaultUnit={embedHeightUnit}
minValue={1} minValue={1}
maxValue={99999} maxValue={99999}
units={unitOptions} units={unitOptions}
/> />
</div> </div>
</>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1930,9 +1930,21 @@
} }
} }
.media-embed-wrap {
display: block;
width: 100%;
height: 100%;
background: #000;
.media-embed-wrap { .media-embed-wrap {
display: block; display: block;
.player-container,
.player-container-inner {
width: 100%;
height: 100%;
padding-top: 0;
background: #000;
}
.player-container, .player-container,
.player-container-inner { .player-container-inner {
width: 100%; width: 100%;
@@ -1946,6 +1958,10 @@
.circle-icon-button { .circle-icon-button {
} }
.video-js.vjs-mediacms {
padding-top: 0;
}
}
.video-js.vjs-mediacms { .video-js.vjs-mediacms {
padding-top: math.div(9, 16) * 100%; padding-top: math.div(9, 16) * 100%;
} }

View File

@@ -410,8 +410,12 @@ export default class VideoViewer extends React.PureComponent {
poster: this.videoPoster, poster: this.videoPoster,
previewSprite: previewSprite, previewSprite: previewSprite,
subtitlesInfo: this.props.data.subtitles_info, subtitlesInfo: this.props.data.subtitles_info,
enableAutoplay: !this.props.inEmbed,
inEmbed: this.props.inEmbed, inEmbed: this.props.inEmbed,
showTitle: this.props.showTitle,
showRelated: this.props.showRelated,
showUserAvatar: this.props.showUserAvatar,
linkTitle: this.props.linkTitle,
urlTimestamp: this.props.timestamp,
hasTheaterMode: !this.props.inEmbed, hasTheaterMode: !this.props.inEmbed,
hasNextLink: !!nextLink, hasNextLink: !!nextLink,
nextLink: nextLink, nextLink: nextLink,
@@ -435,9 +439,19 @@ export default class VideoViewer extends React.PureComponent {
VideoViewer.defaultProps = { VideoViewer.defaultProps = {
inEmbed: !0, inEmbed: !0,
showTitle: !0,
showRelated: !0,
showUserAvatar: !0,
linkTitle: !0,
timestamp: null,
siteUrl: PropTypes.string.isRequired, siteUrl: PropTypes.string.isRequired,
}; };
VideoViewer.propTypes = { VideoViewer.propTypes = {
inEmbed: PropTypes.bool, inEmbed: PropTypes.bool,
showTitle: PropTypes.bool,
showRelated: PropTypes.bool,
showUserAvatar: PropTypes.bool,
linkTitle: PropTypes.bool,
timestamp: PropTypes.number,
}; };

View File

@@ -41,7 +41,7 @@ export const EmbedPage: React.FC = () => {
}, []); }, []);
return ( return (
<div className="embed-wrap" style={wrapperStyles}> <div className="embed-wrap media-embed-wrap" style={wrapperStyles}>
{failedMediaLoad && ( {failedMediaLoad && (
<div className="player-container player-container-error" style={containerStyles}> <div className="player-container player-container-error" style={containerStyles}>
<div className="player-container-inner" style={containerStyles}> <div className="player-container-inner" style={containerStyles}>
@@ -59,9 +59,32 @@ export const EmbedPage: React.FC = () => {
{loadedVideo && ( {loadedVideo && (
<SiteConsumer> <SiteConsumer>
{(site) => ( {(site) => {
<VideoViewer data={MediaPageStore.get('media-data')} siteUrl={site.url} containerStyles={containerStyles} /> const urlParams = new URLSearchParams(window.location.search);
)} const urlShowTitle = urlParams.get('showTitle');
const showTitle = urlShowTitle !== '0';
const urlShowRelated = urlParams.get('showRelated');
const showRelated = urlShowRelated !== '0';
const urlShowUserAvatar = urlParams.get('showUserAvatar');
const showUserAvatar = urlShowUserAvatar !== '0';
const urlLinkTitle = urlParams.get('linkTitle');
const linkTitle = urlLinkTitle !== '0';
const urlTimestamp = urlParams.get('t');
const timestamp = urlTimestamp ? parseInt(urlTimestamp, 10) : null;
return (
<VideoViewer
data={MediaPageStore.get('media-data')}
siteUrl={site.url}
containerStyles={containerStyles}
showTitle={showTitle}
showRelated={showRelated}
showUserAvatar={showUserAvatar}
linkTitle={linkTitle}
timestamp={timestamp}
/>
);
}}
</SiteConsumer> </SiteConsumer>
)} )}
</div> </div>

6902
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

13
package.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "mediacms",
"version": "7.5.0",
"devDependencies": {
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^11.0.3",
"@semantic-release/release-notes-generator": "^14.0.3",
"conventional-changelog-conventionalcommits": "^9.0.0",
"semantic-release": "^24.2.6",
"semantic-release-replace-plugin": "^1.2.7"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long