mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-05-05 12:13:26 -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/cms/*
|
||||||
/templates/*.html
|
/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>
|
</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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
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