mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-04-23 15:58:08 -04:00
merge main
This commit is contained in:
@@ -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 }}
|
||||
@@ -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 }}
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
/templates/cms/*
|
||||
/templates/*.html
|
||||
*.scss
|
||||
*.scss
|
||||
/frontend/
|
||||
+100
@@ -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}"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -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))
|
||||
+1
-1
@@ -1 +1 @@
|
||||
VERSION = "8.10"
|
||||
VERSION = "7.6"
|
||||
|
||||
@@ -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>
|
||||
@@ -204,6 +204,54 @@ class SeekIndicator extends Component {
|
||||
</div>
|
||||
`;
|
||||
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
|
||||
@@ -239,6 +287,11 @@ class SeekIndicator extends Component {
|
||||
this.showTimeout = setTimeout(() => {
|
||||
this.hide();
|
||||
}, 500);
|
||||
} else if (direction === 'copy-url' || direction === 'copy-embed') {
|
||||
// Copy operations: 500ms (same as play/pause)
|
||||
this.showTimeout = setTimeout(() => {
|
||||
this.hide();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,22 @@ class EmbedInfoOverlay extends Component {
|
||||
this.authorThumbnail = options.authorThumbnail || '';
|
||||
this.videoTitle = options.videoTitle || 'Video';
|
||||
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
|
||||
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
|
||||
if (this.authorThumbnail) {
|
||||
if (this.authorThumbnail && this.showUserAvatar) {
|
||||
const avatarContainer = document.createElement('div');
|
||||
avatarContainer.className = 'embed-avatar-container';
|
||||
avatarContainer.style.cssText = `
|
||||
@@ -125,7 +137,7 @@ class EmbedInfoOverlay extends Component {
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
if (this.videoUrl) {
|
||||
if (this.videoUrl && this.linkTitle) {
|
||||
const titleLink = document.createElement('a');
|
||||
titleLink.href = this.videoUrl;
|
||||
titleLink.target = '_blank';
|
||||
@@ -186,10 +198,16 @@ class EmbedInfoOverlay extends Component {
|
||||
const player = this.player();
|
||||
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
|
||||
const updateOverlayVisibility = () => {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
|
||||
if (!player.hasStarted()) {
|
||||
// Show overlay when video hasn't started (poster is showing) - like before
|
||||
overlay.style.opacity = '1';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 'video.js/dist/video-js.css';
|
||||
import '../../styles/embed.css';
|
||||
@@ -17,6 +17,7 @@ import CustomRemainingTime from '../controls/CustomRemainingTime';
|
||||
import CustomChaptersOverlay from '../controls/CustomChaptersOverlay';
|
||||
import CustomSettingsMenu from '../controls/CustomSettingsMenu';
|
||||
import SeekIndicator from '../controls/SeekIndicator';
|
||||
import VideoContextMenu from '../overlays/VideoContextMenu';
|
||||
import UserPreferences from '../../utils/UserPreferences';
|
||||
import PlayerConfig from '../../config/playerConfig';
|
||||
import { AutoplayHandler } from '../../utils/AutoplayHandler';
|
||||
@@ -169,7 +170,7 @@ const enableStandardButtonTooltips = (player) => {
|
||||
}, 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 playerRef = useRef(null); // Track the player 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 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)
|
||||
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
|
||||
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(
|
||||
() =>
|
||||
typeof window !== 'undefined' && window.MEDIA_DATA
|
||||
@@ -214,12 +207,37 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
},
|
||||
siteUrl: 'https://deic.mediacms.io',
|
||||
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
|
||||
// Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON
|
||||
// CONDITIONAL LOGIC:
|
||||
@@ -531,8 +549,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
isPlayList: mediaData?.isPlayList,
|
||||
related_media: mediaData.data?.related_media || [],
|
||||
nextLink: mediaData?.nextLink || null,
|
||||
urlAutoplay: mediaData?.urlAutoplay || true,
|
||||
urlMuted: mediaData?.urlMuted || false,
|
||||
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(() => {
|
||||
// Only initialize if we don't already have a player and element exists
|
||||
if (videoRef.current && !playerRef.current) {
|
||||
@@ -1078,6 +1300,9 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
currentVideo,
|
||||
relatedVideos,
|
||||
goToNextVideo,
|
||||
showRelated: finalShowRelated,
|
||||
showUserAvatar: finalShowUserAvatar,
|
||||
linkTitle: finalLinkTitle,
|
||||
});
|
||||
customComponents.current.endScreenHandler = endScreenHandler; // Store for cleanup
|
||||
|
||||
@@ -1098,8 +1323,8 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
}
|
||||
|
||||
// Handle URL timestamp parameter
|
||||
if (mediaData.urlTimestamp !== null && mediaData.urlTimestamp >= 0) {
|
||||
const timestamp = mediaData.urlTimestamp;
|
||||
if (finalTimestamp !== null && finalTimestamp >= 0) {
|
||||
const timestamp = finalTimestamp;
|
||||
|
||||
// Wait for video metadata to be loaded before seeking
|
||||
if (playerRef.current.readyState() >= 1) {
|
||||
@@ -1997,6 +2222,10 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
authorThumbnail: currentVideo.author_thumbnail,
|
||||
videoTitle: currentVideo.title,
|
||||
videoUrl: currentVideo.url,
|
||||
showTitle: finalShowTitle,
|
||||
showRelated: finalShowRelated,
|
||||
showUserAvatar: finalShowUserAvatar,
|
||||
linkTitle: finalLinkTitle,
|
||||
});
|
||||
}
|
||||
// END: Add Embed Info Overlay Component
|
||||
@@ -2083,52 +2312,113 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
// Make the video element focusable
|
||||
const videoElement = playerRef.current.el();
|
||||
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);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<video
|
||||
ref={videoRef}
|
||||
id={videoId}
|
||||
controls={true}
|
||||
className={`video-js vjs-fluid vjs-default-skin${currentVideo.useRoundedCorners ? ' video-js-rounded-corners' : ''}`}
|
||||
preload="auto"
|
||||
poster={currentVideo.poster}
|
||||
tabIndex="0"
|
||||
>
|
||||
{/* <source src="/videos/sample-video.mp4" type="video/mp4" />
|
||||
<source src="/videos/sample-video.webm" type="video/webm" /> */}
|
||||
<p className="vjs-no-js">
|
||||
To view this video please enable JavaScript, and consider upgrading to a web browser that
|
||||
<a href="https://videojs.com/html5-video-support/" target="_blank">
|
||||
supports HTML5 video
|
||||
</a>
|
||||
</p>
|
||||
<>
|
||||
<video
|
||||
ref={videoRef}
|
||||
id={videoId}
|
||||
controls={true}
|
||||
className={`video-js ${isEmbedPlayer ? 'vjs-fill' : 'vjs-fluid'} vjs-default-skin${currentVideo.useRoundedCorners ? ' video-js-rounded-corners' : ''}`}
|
||||
preload="auto"
|
||||
poster={currentVideo.poster}
|
||||
tabIndex="0"
|
||||
>
|
||||
{/* <source src="/videos/sample-video.mp4" type="video/mp4" />
|
||||
<source src="/videos/sample-video.webm" type="video/webm" /> */}
|
||||
<p className="vjs-no-js">
|
||||
To view this video please enable JavaScript, and consider upgrading to a web browser that
|
||||
<a href="https://videojs.com/html5-video-support/" target="_blank">
|
||||
supports HTML5 video
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{/* Add subtitle tracks */}
|
||||
{/* {subtitleTracks &&
|
||||
subtitleTracks.map((track, index) => (
|
||||
<track
|
||||
key={index}
|
||||
kind={track.kind}
|
||||
src={track.src}
|
||||
srcLang={track.srclang}
|
||||
label={track.label}
|
||||
default={track.default}
|
||||
/>
|
||||
))} */}
|
||||
{/*
|
||||
<track kind="chapters" src="/sample-chapters.vtt" /> */}
|
||||
{/* Add chapters track */}
|
||||
{/* {chaptersData &&
|
||||
chaptersData.length > 0 &&
|
||||
(console.log('chaptersData', chaptersData), (<track kind="chapters" src="/sample-chapters.vtt" />))} */}
|
||||
</video>
|
||||
{/* Add subtitle tracks */}
|
||||
{/* {subtitleTracks &&
|
||||
subtitleTracks.map((track, index) => (
|
||||
<track
|
||||
key={index}
|
||||
kind={track.kind}
|
||||
src={track.src}
|
||||
srcLang={track.srclang}
|
||||
label={track.label}
|
||||
default={track.default}
|
||||
/>
|
||||
))} */}
|
||||
{/*
|
||||
<track kind="chapters" src="/sample-chapters.vtt" /> */}
|
||||
{/* Add chapters track */}
|
||||
{/* {chaptersData &&
|
||||
chaptersData.length > 0 &&
|
||||
(console.log('chaptersData', chaptersData), (<track kind="chapters" src="/sample-chapters.vtt" />))} */}
|
||||
</video>
|
||||
<VideoContextMenu
|
||||
visible={contextMenuVisible}
|
||||
position={contextMenuPosition}
|
||||
onClose={closeContextMenu}
|
||||
onCopyVideoUrl={handleCopyVideoUrl}
|
||||
onCopyVideoUrlAtTime={handleCopyVideoUrlAtTime}
|
||||
onCopyEmbedCode={handleCopyEmbedCode}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,17 @@ export class EndScreenHandler {
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
setTimeout(() => {
|
||||
if (this.player && !this.player.isDisposed()) {
|
||||
|
||||
@@ -31,8 +31,11 @@ const VideoJSEmbed = ({
|
||||
poster,
|
||||
previewSprite,
|
||||
subtitlesInfo,
|
||||
enableAutoplay,
|
||||
inEmbed,
|
||||
showTitle,
|
||||
showRelated,
|
||||
showUserAvatar,
|
||||
linkTitle,
|
||||
hasTheaterMode,
|
||||
hasNextLink,
|
||||
nextLink,
|
||||
@@ -62,8 +65,10 @@ const VideoJSEmbed = ({
|
||||
if (typeof window !== 'undefined') {
|
||||
// Get URL parameters for autoplay, muted, and timestamp
|
||||
const urlTimestamp = getUrlParameter('t');
|
||||
const urlAutoplay = getUrlParameter('autoplay');
|
||||
const urlMuted = getUrlParameter('muted');
|
||||
const urlShowRelated = getUrlParameter('showRelated');
|
||||
const urlShowUserAvatar = getUrlParameter('showUserAvatar');
|
||||
const urlLinkTitle = getUrlParameter('linkTitle');
|
||||
|
||||
window.MEDIA_DATA = {
|
||||
data: data || {},
|
||||
@@ -71,7 +76,7 @@ const VideoJSEmbed = ({
|
||||
version: version,
|
||||
isPlayList: isPlayList,
|
||||
playerVolume: playerVolume || 0.5,
|
||||
playerSoundMuted: playerSoundMuted || (urlMuted === '1'),
|
||||
playerSoundMuted: urlMuted === '1',
|
||||
videoQuality: videoQuality || 'auto',
|
||||
videoPlaybackSpeed: videoPlaybackSpeed || 1,
|
||||
inTheaterMode: inTheaterMode || false,
|
||||
@@ -83,8 +88,11 @@ const VideoJSEmbed = ({
|
||||
poster: poster || '',
|
||||
previewSprite: previewSprite || null,
|
||||
subtitlesInfo: subtitlesInfo || [],
|
||||
enableAutoplay: enableAutoplay || (urlAutoplay === '1'),
|
||||
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,
|
||||
hasNextLink: hasNextLink || false,
|
||||
nextLink: nextLink || null,
|
||||
@@ -92,8 +100,10 @@ const VideoJSEmbed = ({
|
||||
errorMessage: errorMessage || '',
|
||||
// URL parameters
|
||||
urlTimestamp: urlTimestamp ? parseInt(urlTimestamp, 10) : null,
|
||||
urlAutoplay: urlAutoplay === '1',
|
||||
urlMuted: urlMuted === '1',
|
||||
urlShowRelated: urlShowRelated === '1' || urlShowRelated === 'true',
|
||||
urlShowUserAvatar: urlShowUserAvatar === '1' || urlShowUserAvatar === 'true',
|
||||
urlLinkTitle: urlLinkTitle === '1' || urlLinkTitle === 'true',
|
||||
onClickNextCallback: onClickNextCallback || null,
|
||||
onClickPreviousCallback: onClickPreviousCallback || null,
|
||||
onStateUpdateCallback: onStateUpdateCallback || null,
|
||||
@@ -176,11 +186,17 @@ const VideoJSEmbed = ({
|
||||
// Scroll to the video player with smooth behavior
|
||||
const videoElement = document.querySelector(inEmbedRef.current ? '#video-embed' : '#video-main');
|
||||
if (videoElement) {
|
||||
videoElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest'
|
||||
});
|
||||
const urlScroll = getUrlParameter('scroll');
|
||||
const isIframe = window.parent !== window;
|
||||
|
||||
// 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 {
|
||||
console.warn('VideoJS player not found for timestamp navigation');
|
||||
@@ -220,7 +236,14 @@ const VideoJSEmbed = ({
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,10 +4,32 @@ import { LinksContext, SiteConsumer } from '../../utils/contexts/';
|
||||
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
||||
import { PageActions, MediaPageActions } from '../../utils/actions/';
|
||||
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) {
|
||||
const embedVideoDimensions = PageStore.get('config-options').embedded.video.dimensions;
|
||||
const savedOptions = loadEmbedOptions();
|
||||
|
||||
const links = useContext(LinksContext);
|
||||
|
||||
@@ -18,12 +40,19 @@ export function MediaShareEmbed(props) {
|
||||
const onRightBottomRef = useRef(null);
|
||||
|
||||
const [maxHeight, setMaxHeight] = useState(window.innerHeight - 144 + 56);
|
||||
const [keepAspectRatio, setKeepAspectRatio] = useState(false);
|
||||
const [aspectRatio, setAspectRatio] = useState('16:9');
|
||||
const [embedWidthValue, setEmbedWidthValue] = useState(embedVideoDimensions.width);
|
||||
const [embedWidthUnit, setEmbedWidthUnit] = useState(embedVideoDimensions.widthUnit);
|
||||
const [embedHeightValue, setEmbedHeightValue] = useState(embedVideoDimensions.height);
|
||||
const [embedHeightUnit, setEmbedHeightUnit] = useState(embedVideoDimensions.heightUnit);
|
||||
const [keepAspectRatio, setKeepAspectRatio] = useState(savedOptions?.keepAspectRatio ?? true);
|
||||
const [showTitle, setShowTitle] = useState(savedOptions?.showTitle ?? true);
|
||||
const [showRelated, setShowRelated] = useState(savedOptions?.showRelated ?? true);
|
||||
const [showUserAvatar, setShowUserAvatar] = useState(savedOptions?.showUserAvatar ?? true);
|
||||
const [linkTitle, setLinkTitle] = useState(savedOptions?.linkTitle ?? true);
|
||||
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 [rightMiddlePositionBottom, setRightMiddlePositionBottom] = useState(60);
|
||||
const [unitOptions, setUnitOptions] = useState([
|
||||
@@ -71,36 +100,65 @@ export function MediaShareEmbed(props) {
|
||||
setEmbedHeightUnit(newVal);
|
||||
}
|
||||
|
||||
function onKeepAspectRatioChange() {
|
||||
const newVal = !keepAspectRatio;
|
||||
function onShowTitleChange() {
|
||||
setShowTitle(!showTitle);
|
||||
}
|
||||
|
||||
const arr = aspectRatio.split(':');
|
||||
const x = arr[0];
|
||||
const y = arr[1];
|
||||
function onShowRelatedChange() {
|
||||
setShowRelated(!showRelated);
|
||||
}
|
||||
|
||||
setKeepAspectRatio(newVal);
|
||||
setEmbedWidthUnit(newVal ? 'px' : embedWidthUnit);
|
||||
setEmbedHeightUnit(newVal ? 'px' : embedHeightUnit);
|
||||
setEmbedHeightValue(newVal ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue);
|
||||
setUnitOptions(
|
||||
newVal
|
||||
? [{ key: 'px', label: 'px' }]
|
||||
: [
|
||||
{ key: 'px', label: 'px' },
|
||||
{ key: 'percent', label: '%' },
|
||||
]
|
||||
);
|
||||
function onShowUserAvatarChange() {
|
||||
setShowUserAvatar(!showUserAvatar);
|
||||
}
|
||||
|
||||
function onLinkTitleChange() {
|
||||
setLinkTitle(!linkTitle);
|
||||
}
|
||||
|
||||
function onResponsiveChange() {
|
||||
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() {
|
||||
const newVal = aspectRatioValueRef.current.value;
|
||||
|
||||
const arr = newVal.split(':');
|
||||
const x = arr[0];
|
||||
const y = arr[1];
|
||||
if (newVal === 'custom') {
|
||||
setAspectRatio(newVal);
|
||||
setKeepAspectRatio(false);
|
||||
} else {
|
||||
const arr = newVal.split(':');
|
||||
const x = arr[0];
|
||||
const y = arr[1];
|
||||
|
||||
setAspectRatio(newVal);
|
||||
setEmbedHeightValue(keepAspectRatio ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue);
|
||||
setAspectRatio(newVal);
|
||||
setKeepAspectRatio(true);
|
||||
setEmbedHeightValue(parseInt((embedWidthValue * y) / x, 10));
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="share-embed" style={{ maxHeight: maxHeight + 'px' }}>
|
||||
<div className="share-embed-inner">
|
||||
<div className="on-left">
|
||||
<div className="media-embed-wrap">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,16 +291,7 @@ export function MediaShareEmbed(props) {
|
||||
>
|
||||
<textarea
|
||||
readOnly
|
||||
value={
|
||||
'<iframe width="' +
|
||||
('percent' === embedWidthUnit ? embedWidthValue + '%' : embedWidthValue) +
|
||||
'" height="' +
|
||||
('percent' === embedHeightUnit ? embedHeightValue + '%' : embedHeightValue) +
|
||||
'" src="' +
|
||||
links.embed +
|
||||
MediaPageStore.get('media-id') +
|
||||
'" frameborder="0" allowfullscreen></iframe>'
|
||||
}
|
||||
value={getEmbedCode()}
|
||||
></textarea>
|
||||
|
||||
<div className="iframe-config">
|
||||
@@ -179,59 +303,106 @@ export function MediaShareEmbed(props) {
|
||||
</div>*/}
|
||||
|
||||
<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">
|
||||
<label style={{ minHeight: '36px' }}>
|
||||
<input type="checkbox" checked={keepAspectRatio} onChange={onKeepAspectRatioChange} />
|
||||
Keep aspect ratio
|
||||
<label style={{ minHeight: '36px', whiteSpace: 'nowrap' }}>
|
||||
<input type="checkbox" checked={showTitle} onChange={onShowTitleChange} />
|
||||
Show title
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!keepAspectRatio ? null : (
|
||||
<div className="options-group">
|
||||
<select ref={aspectRatioValueRef} onChange={onAspectRatioChange} value={aspectRatio}>
|
||||
<optgroup label="Horizontal orientation">
|
||||
<option value="16:9">16:9</option>
|
||||
<option value="4:3">4:3</option>
|
||||
<option value="3:2">3:2</option>
|
||||
</optgroup>
|
||||
<optgroup label="Vertical orientation">
|
||||
<option value="9:16">9:16</option>
|
||||
<option value="3:4">3:4</option>
|
||||
<option value="2:3">2:3</option>
|
||||
</optgroup>
|
||||
<div className="options-group">
|
||||
<label style={{ minHeight: '36px', whiteSpace: 'nowrap', opacity: showTitle ? 1 : 0.5 }}>
|
||||
<input type="checkbox" checked={linkTitle} onChange={onLinkTitleChange} disabled={!showTitle} />
|
||||
Link title
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="options-group">
|
||||
<label style={{ minHeight: '36px', whiteSpace: 'nowrap' }}>
|
||||
<input type="checkbox" checked={showRelated} onChange={onShowRelatedChange} />
|
||||
Show related
|
||||
</label>
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="options-group">
|
||||
<NumericInputWithUnit
|
||||
valueCallback={onEmbedWidthValueChange}
|
||||
unitCallback={onEmbedWidthUnitChange}
|
||||
label={'Width'}
|
||||
defaultValue={parseInt(embedWidthValue, 10)}
|
||||
defaultUnit={embedWidthUnit}
|
||||
minValue={1}
|
||||
maxValue={99999}
|
||||
units={unitOptions}
|
||||
/>
|
||||
</div>
|
||||
{!responsive && (
|
||||
<>
|
||||
<div className="options-group">
|
||||
<NumericInputWithUnit
|
||||
valueCallback={onEmbedWidthValueChange}
|
||||
unitCallback={onEmbedWidthUnitChange}
|
||||
label={'Width'}
|
||||
defaultValue={parseInt(embedWidthValue, 10)}
|
||||
defaultUnit={embedWidthUnit}
|
||||
minValue={1}
|
||||
maxValue={99999}
|
||||
units={unitOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="options-group">
|
||||
<NumericInputWithUnit
|
||||
valueCallback={onEmbedHeightValueChange}
|
||||
unitCallback={onEmbedHeightUnitChange}
|
||||
label={'Height'}
|
||||
defaultValue={parseInt(embedHeightValue, 10)}
|
||||
defaultUnit={embedHeightUnit}
|
||||
minValue={1}
|
||||
maxValue={99999}
|
||||
units={unitOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="options-group">
|
||||
<NumericInputWithUnit
|
||||
valueCallback={onEmbedHeightValueChange}
|
||||
unitCallback={onEmbedHeightUnitChange}
|
||||
label={'Height'}
|
||||
defaultValue={parseInt(embedHeightValue, 10)}
|
||||
defaultUnit={embedHeightUnit}
|
||||
minValue={1}
|
||||
maxValue={99999}
|
||||
units={unitOptions}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1930,9 +1930,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.media-embed-wrap {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
.media-embed-wrap {
|
||||
display: block;
|
||||
|
||||
.player-container,
|
||||
.player-container-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 0;
|
||||
background: #000;
|
||||
}
|
||||
.player-container,
|
||||
.player-container-inner {
|
||||
width: 100%;
|
||||
@@ -1946,6 +1958,10 @@
|
||||
.circle-icon-button {
|
||||
}
|
||||
|
||||
.video-js.vjs-mediacms {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
.video-js.vjs-mediacms {
|
||||
padding-top: math.div(9, 16) * 100%;
|
||||
}
|
||||
|
||||
@@ -410,8 +410,12 @@ export default class VideoViewer extends React.PureComponent {
|
||||
poster: this.videoPoster,
|
||||
previewSprite: previewSprite,
|
||||
subtitlesInfo: this.props.data.subtitles_info,
|
||||
enableAutoplay: !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,
|
||||
hasNextLink: !!nextLink,
|
||||
nextLink: nextLink,
|
||||
@@ -435,9 +439,19 @@ export default class VideoViewer extends React.PureComponent {
|
||||
|
||||
VideoViewer.defaultProps = {
|
||||
inEmbed: !0,
|
||||
showTitle: !0,
|
||||
showRelated: !0,
|
||||
showUserAvatar: !0,
|
||||
linkTitle: !0,
|
||||
timestamp: null,
|
||||
siteUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
VideoViewer.propTypes = {
|
||||
inEmbed: PropTypes.bool,
|
||||
showTitle: PropTypes.bool,
|
||||
showRelated: PropTypes.bool,
|
||||
showUserAvatar: PropTypes.bool,
|
||||
linkTitle: PropTypes.bool,
|
||||
timestamp: PropTypes.number,
|
||||
};
|
||||
@@ -41,7 +41,7 @@ export const EmbedPage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="embed-wrap" style={wrapperStyles}>
|
||||
<div className="embed-wrap media-embed-wrap" style={wrapperStyles}>
|
||||
{failedMediaLoad && (
|
||||
<div className="player-container player-container-error" style={containerStyles}>
|
||||
<div className="player-container-inner" style={containerStyles}>
|
||||
@@ -59,9 +59,32 @@ export const EmbedPage: React.FC = () => {
|
||||
|
||||
{loadedVideo && (
|
||||
<SiteConsumer>
|
||||
{(site) => (
|
||||
<VideoViewer data={MediaPageStore.get('media-data')} siteUrl={site.url} containerStyles={containerStyles} />
|
||||
)}
|
||||
{(site) => {
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Generated
+6902
File diff suppressed because it is too large
Load Diff
@@ -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
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+203
-161
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user