mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-03-13 16:32:22 -04:00
Compare commits
9 Commits
feat-lti-i
...
v7.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7c675596f | ||
|
|
36d815c0cf | ||
|
|
8f28b00a63 | ||
|
|
74952f68d7 | ||
|
|
7950a4655a | ||
|
|
b76282f9e4 | ||
|
|
b405a04e34 | ||
|
|
76a27ae256 | ||
|
|
223e87073f |
22
.github/workflows/semantic-pull-request.yaml
vendored
Normal file
22
.github/workflows/semantic-pull-request.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: "Lint PR"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
name: Validate PR title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: dev
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
47
.github/workflows/semantic-release.yaml
vendored
Normal file
47
.github/workflows/semantic-release.yaml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Semantic Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
semantic-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: dev
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup SSH
|
||||||
|
uses: webfactory/ssh-agent@v0.8.0
|
||||||
|
with:
|
||||||
|
ssh-private-key: ${{ secrets.GA_DEPLOY_KEY }}
|
||||||
|
|
||||||
|
# use SSH url to ensure git commit using a deploy key bypasses the main
|
||||||
|
# branch protection rule
|
||||||
|
- name: Configure Git for SSH Push
|
||||||
|
run: git remote set-url origin "git@github.com:${{ github.repository }}.git"
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm clean-install
|
||||||
|
|
||||||
|
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
|
||||||
|
run: npm audit signatures
|
||||||
|
|
||||||
|
- name: Run Semantic Release
|
||||||
|
run: npx semantic-release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
/templates/cms/*
|
/templates/cms/*
|
||||||
/templates/*.html
|
/templates/*.html
|
||||||
*.scss
|
*.scss
|
||||||
|
/frontend/
|
||||||
100
.releaserc.json
Normal file
100
.releaserc.json
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
{
|
||||||
|
"preset": "conventionalcommits"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
{
|
||||||
|
"preset": "conventionalcommits",
|
||||||
|
"presetConfig": {
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "feat",
|
||||||
|
"section": "Features"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fix",
|
||||||
|
"section": "Bug Fixes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chore",
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "docs",
|
||||||
|
"section": "Documentation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "style",
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "refactor",
|
||||||
|
"section": "Refactors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "perf",
|
||||||
|
"section": "Performance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "test",
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "depr",
|
||||||
|
"section": "Deprecations"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"semantic-release-replace-plugin",
|
||||||
|
{
|
||||||
|
"replacements": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"from": "\"version\": \".*\"",
|
||||||
|
"to": "\"version\": \"${nextRelease.version}\"",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"file": "package.json",
|
||||||
|
"hasChanged": true,
|
||||||
|
"numMatches": 1,
|
||||||
|
"numReplacements": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"countMatches": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
{
|
||||||
|
"changelogFile": "CHANGELOG.md",
|
||||||
|
"changelogTitle": "# Changelog"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/github",
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"package.json",
|
||||||
|
"CHANGELOG.md"
|
||||||
|
],
|
||||||
|
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
37
CHANGELOG.md
Normal file
37
CHANGELOG.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [7.5.0](https://github.com/mediacms-io/mediacms/compare/v7.4.0...v7.5.0) (2026-02-06)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* bump version ([36d815c](https://github.com/mediacms-io/mediacms/commit/36d815c0cfbe21d3136541d410d545742b9ebecd))
|
||||||
|
|
||||||
|
## [7.4.0](https://github.com/mediacms-io/mediacms/compare/v7.3.0...v7.4.0) (2026-02-06)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add video player context menu with share/embed options ([#1472](https://github.com/mediacms-io/mediacms/issues/1472)) ([74952f6](https://github.com/mediacms-io/mediacms/commit/74952f68d79bc67617edb38eac62d2f5e7457565))
|
||||||
|
|
||||||
|
## [7.3.0](https://github.com/mediacms-io/mediacms/compare/v7.2.0...v7.3.0) (2026-02-06)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add package json for semantic release ([b405a04](https://github.com/mediacms-io/mediacms/commit/b405a04e346ca81b7d3f4e099eb984e7785cdd0f))
|
||||||
|
* add semantic release github actions ([76a27ae](https://github.com/mediacms-io/mediacms/commit/76a27ae25609178c1bd47c947b9f1a082c791d61))
|
||||||
|
* frontend unit tests ([1c15880](https://github.com/mediacms-io/mediacms/commit/1c15880ae3ef1ce77f53d5b473dfc0cc448b4977))
|
||||||
|
* Implement persistent "Embed Mode" to hide UI shell via Session Storage ([#1484](https://github.com/mediacms-io/mediacms/issues/1484)) ([223e870](https://github.com/mediacms-io/mediacms/commit/223e87073f7d5e44130c9976854cac670db0ae66))
|
||||||
|
* Improve Visual Distinction Between Trim and Chapters Editors ([#1445](https://github.com/mediacms-io/mediacms/issues/1445)) ([d9b1d6c](https://github.com/mediacms-io/mediacms/commit/d9b1d6cab1d2bdfc16f799a0a27b64313e2e0d22))
|
||||||
|
* semantic release ([b76282f](https://github.com/mediacms-io/mediacms/commit/b76282f9e465a39c2da5e9a22184d1db23de3f56))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add delay to task creation ([1b3cdfd](https://github.com/mediacms-io/mediacms/commit/1b3cdfd302abc5e69ebe01ca52b5091f3b24c0b2))
|
||||||
|
* Add regex denoter and improve celerybeat gitignore ([#1446](https://github.com/mediacms-io/mediacms/issues/1446)) ([90331f3](https://github.com/mediacms-io/mediacms/commit/90331f3b4a2a5737de9dd75ab45c096944813c42))
|
||||||
|
* adjust poster url for audio ([01912ea](https://github.com/mediacms-io/mediacms/commit/01912ea1f99ef43793a65712539d6264f1f6410f))
|
||||||
|
* Chapter numbering and preserve custom titles on segment reorder ([#1435](https://github.com/mediacms-io/mediacms/issues/1435)) ([cd7dd4f](https://github.com/mediacms-io/mediacms/commit/cd7dd4f72c9f0bac466c680f686a9ecfdd3a38dd))
|
||||||
|
* Show default chapter names in textarea instead of placeholder text ([#1428](https://github.com/mediacms-io/mediacms/issues/1428)) ([5eb6faf](https://github.com/mediacms-io/mediacms/commit/5eb6fafb8c6928b8bc3fe5f0c7af315273f78a55))
|
||||||
|
* static files ([#1429](https://github.com/mediacms-io/mediacms/issues/1429)) ([ba2c31b](https://github.com/mediacms-io/mediacms/commit/ba2c31b1e65b7f508dee598b1f2d86f01f9bf036))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* update page link ([aeef828](https://github.com/mediacms-io/mediacms/commit/aeef8284bfba2a9a7f69c684f96c54f0e0e0cf92))
|
||||||
@@ -1 +1 @@
|
|||||||
VERSION = "7.6"
|
VERSION = "7.5"
|
||||||
|
|||||||
34
frontend-tools/video-js/examples/full-screen-video.html
Normal file
34
frontend-tools/video-js/examples/full-screen-video.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" style="height: 100%">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Embedded Video - Full Screen</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<iframe
|
||||||
|
src="https://demo.mediacms.io/embed?m=zK2nirNLC"
|
||||||
|
style="
|
||||||
|
width: 100%;
|
||||||
|
max-width: calc(100vh * 16 / 9);
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
border: 0;
|
||||||
|
"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,257 +3,278 @@ import { SiteContext } from '../../utils/contexts/';
|
|||||||
import { useUser, usePopup } from '../../utils/hooks/';
|
import { useUser, usePopup } from '../../utils/hooks/';
|
||||||
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 { formatInnerLink, publishedOnDate } from '../../utils/helpers/';
|
import { formatInnerLink, inEmbeddedApp, publishedOnDate } from '../../utils/helpers/';
|
||||||
import { PopupMain } from '../_shared/';
|
import { PopupMain } from '../_shared/';
|
||||||
import CommentsList from '../comments/Comments';
|
import CommentsList from '../comments/Comments';
|
||||||
import { replaceString } from '../../utils/helpers/';
|
import { replaceString } from '../../utils/helpers/';
|
||||||
import { translateString } from '../../utils/helpers/';
|
import { translateString } from '../../utils/helpers/';
|
||||||
|
|
||||||
function metafield(arr) {
|
function metafield(arr) {
|
||||||
let i;
|
let i;
|
||||||
let sep;
|
let sep;
|
||||||
let ret = [];
|
let ret = [];
|
||||||
|
|
||||||
if (arr && arr.length) {
|
if (arr && arr.length) {
|
||||||
i = 0;
|
i = 0;
|
||||||
sep = 1 < arr.length ? ', ' : '';
|
sep = 1 < arr.length ? ', ' : '';
|
||||||
while (i < arr.length) {
|
while (i < arr.length) {
|
||||||
ret[i] = (
|
ret[i] = (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
<a href={arr[i].url} title={arr[i].title}>
|
<a href={arr[i].url} title={arr[i].title}>
|
||||||
{arr[i].title}
|
{arr[i].title}
|
||||||
</a>
|
</a>
|
||||||
{i < arr.length - 1 ? sep : ''}
|
{i < arr.length - 1 ? sep : ''}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
i += 1;
|
i += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MediaAuthorBanner(props) {
|
function MediaAuthorBanner(props) {
|
||||||
return (
|
return (
|
||||||
<div className="media-author-banner">
|
<div className="media-author-banner">
|
||||||
<div>
|
<div>
|
||||||
<a className="author-banner-thumb" href={props.link || null} title={props.name}>
|
<a className="author-banner-thumb" href={props.link || null} title={props.name}>
|
||||||
<span style={{ backgroundImage: 'url(' + props.thumb + ')' }}>
|
<span style={{ backgroundImage: 'url(' + props.thumb + ')' }}>
|
||||||
<img src={props.thumb} loading="lazy" alt={props.name} title={props.name} />
|
<img src={props.thumb} loading="lazy" alt={props.name} title={props.name} />
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
<a href={props.link} className="author-banner-name" title={props.name}>
|
<a href={props.link} className="author-banner-name" title={props.name}>
|
||||||
<span>{props.name}</span>
|
<span>{props.name}</span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
{PageStore.get('config-media-item').displayPublishDate && props.published ? (
|
{PageStore.get('config-media-item').displayPublishDate && props.published ? (
|
||||||
<span className="author-banner-date">
|
<span className="author-banner-date">
|
||||||
{translateString('Published on')} {replaceString(publishedOnDate(new Date(props.published)))}
|
{translateString('Published on')} {replaceString(publishedOnDate(new Date(props.published)))}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MediaMetaField(props) {
|
function MediaMetaField(props) {
|
||||||
return (
|
return (
|
||||||
<div className={props.id.trim() ? 'media-content-' + props.id.trim() : null}>
|
<div className={props.id.trim() ? 'media-content-' + props.id.trim() : null}>
|
||||||
<div className="media-content-field">
|
<div className="media-content-field">
|
||||||
<div className="media-content-field-label">
|
<div className="media-content-field-label">
|
||||||
<h4>{props.title}</h4>
|
<h4>{props.title}</h4>
|
||||||
|
</div>
|
||||||
|
<div className="media-content-field-content">{props.value}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="media-content-field-content">{props.value}</div>
|
);
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditMediaButton(props) {
|
function EditMediaButton(props) {
|
||||||
let link = props.link;
|
let link = props.link;
|
||||||
|
|
||||||
if (window.MediaCMS.site.devEnv) {
|
if (window.MediaCMS.site.devEnv) {
|
||||||
link = '/edit-media.html';
|
link = '/edit-media.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={link} rel="nofollow" title={translateString('Edit media')} className="edit-media-icon">
|
<a href={link} rel="nofollow" title={translateString('Edit media')} className="edit-media-icon">
|
||||||
<i className="material-icons">edit</i>
|
<i className="material-icons">edit</i>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ViewerInfoContent(props) {
|
export default function ViewerInfoContent(props) {
|
||||||
const { userCan } = useUser();
|
const { userCan } = useUser();
|
||||||
|
|
||||||
const description = props.description.trim();
|
const description = props.description.trim();
|
||||||
const tagsContent =
|
const tagsContent =
|
||||||
!PageStore.get('config-enabled').taxonomies.tags || PageStore.get('config-enabled').taxonomies.tags.enabled
|
!PageStore.get('config-enabled').taxonomies.tags || PageStore.get('config-enabled').taxonomies.tags.enabled
|
||||||
? metafield(MediaPageStore.get('media-tags'))
|
? metafield(MediaPageStore.get('media-tags'))
|
||||||
: [];
|
: [];
|
||||||
const categoriesContent = PageStore.get('config-options').pages.media.categoriesWithTitle
|
const categoriesContent = PageStore.get('config-options').pages.media.categoriesWithTitle
|
||||||
? []
|
? []
|
||||||
: !PageStore.get('config-enabled').taxonomies.categories ||
|
: !PageStore.get('config-enabled').taxonomies.categories ||
|
||||||
PageStore.get('config-enabled').taxonomies.categories.enabled
|
PageStore.get('config-enabled').taxonomies.categories.enabled
|
||||||
? metafield(MediaPageStore.get('media-categories'))
|
? metafield(MediaPageStore.get('media-categories'))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
let summary = MediaPageStore.get('media-summary');
|
let summary = MediaPageStore.get('media-summary');
|
||||||
|
|
||||||
summary = summary ? summary.trim() : '';
|
summary = summary ? summary.trim() : '';
|
||||||
|
|
||||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
|
||||||
|
|
||||||
const [hasSummary, setHasSummary] = useState('' !== summary);
|
const [hasSummary, setHasSummary] = useState('' !== summary);
|
||||||
const [isContentVisible, setIsContentVisible] = useState('' == summary);
|
const [isContentVisible, setIsContentVisible] = useState('' == summary);
|
||||||
|
|
||||||
function proceedMediaRemoval() {
|
function proceedMediaRemoval() {
|
||||||
MediaPageActions.removeMedia();
|
MediaPageActions.removeMedia();
|
||||||
popupContentRef.current.toggle();
|
popupContentRef.current.toggle();
|
||||||
}
|
|
||||||
|
|
||||||
function cancelMediaRemoval() {
|
|
||||||
popupContentRef.current.toggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMediaDelete(mediaId) {
|
|
||||||
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
|
||||||
setTimeout(function () {
|
|
||||||
PageActions.addNotification('Media removed. Redirecting...', 'mediaDelete');
|
|
||||||
setTimeout(function () {
|
|
||||||
window.location.href =
|
|
||||||
SiteContext._currentValue.url + '/' + MediaPageStore.get('media-data').author_profile.replace(/^\//g, '');
|
|
||||||
}, 2000);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
if (void 0 !== mediaId) {
|
|
||||||
console.info("Removed media '" + mediaId + '"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMediaDeleteFail(mediaId) {
|
|
||||||
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
|
||||||
setTimeout(function () {
|
|
||||||
PageActions.addNotification('Media removal failed', 'mediaDeleteFail');
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
if (void 0 !== mediaId) {
|
|
||||||
console.info('Media "' + mediaId + '"' + ' removal failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClickLoadMore() {
|
|
||||||
setIsContentVisible(!isContentVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
MediaPageStore.on('media_delete', onMediaDelete);
|
|
||||||
MediaPageStore.on('media_delete_fail', onMediaDeleteFail);
|
|
||||||
return () => {
|
|
||||||
MediaPageStore.removeListener('media_delete', onMediaDelete);
|
|
||||||
MediaPageStore.removeListener('media_delete_fail', onMediaDeleteFail);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const authorLink = formatInnerLink(props.author.url, SiteContext._currentValue.url);
|
|
||||||
const authorThumb = formatInnerLink(props.author.thumb, SiteContext._currentValue.url);
|
|
||||||
|
|
||||||
function setTimestampAnchors(text) {
|
|
||||||
function wrapTimestampWithAnchor(match, string) {
|
|
||||||
let split = match.split(':'),
|
|
||||||
s = 0,
|
|
||||||
m = 1;
|
|
||||||
|
|
||||||
while (split.length > 0) {
|
|
||||||
s += m * parseInt(split.pop(), 10);
|
|
||||||
m *= 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapped = `<a href="#" data-timestamp="${s}" class="video-timestamp">${match}</a>`;
|
|
||||||
return wrapped;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeRegex = new RegExp('((\\d)?\\d:)?(\\d)?\\d:\\d\\d', 'g');
|
function cancelMediaRemoval() {
|
||||||
return text.replace(timeRegex, wrapTimestampWithAnchor);
|
popupContentRef.current.toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
function onMediaDelete(mediaId) {
|
||||||
<div className="media-info-content">
|
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||||
{void 0 === PageStore.get('config-media-item').displayAuthor ||
|
setTimeout(function () {
|
||||||
null === PageStore.get('config-media-item').displayAuthor ||
|
PageActions.addNotification('Media removed. Redirecting...', 'mediaDelete');
|
||||||
!!PageStore.get('config-media-item').displayAuthor ? (
|
setTimeout(function () {
|
||||||
<MediaAuthorBanner link={authorLink} thumb={authorThumb} name={props.author.name} published={props.published} />
|
window.location.href =
|
||||||
) : null}
|
SiteContext._currentValue.url +
|
||||||
|
'/' +
|
||||||
|
MediaPageStore.get('media-data').author_profile.replace(/^\//g, '');
|
||||||
|
}, 2000);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
<div className="media-content-banner">
|
if (void 0 !== mediaId) {
|
||||||
<div className="media-content-banner-inner">
|
console.info("Removed media '" + mediaId + '"');
|
||||||
{hasSummary ? <div className="media-content-summary">{summary}</div> : null}
|
}
|
||||||
{(!hasSummary || isContentVisible) && description ? (
|
}
|
||||||
<div
|
|
||||||
className="media-content-description"
|
|
||||||
dangerouslySetInnerHTML={{ __html: setTimestampAnchors(description) }}
|
|
||||||
></div>
|
|
||||||
) : null}
|
|
||||||
{hasSummary ? (
|
|
||||||
<button className="load-more" onClick={onClickLoadMore}>
|
|
||||||
{isContentVisible ? 'SHOW LESS' : 'SHOW MORE'}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
{tagsContent.length ? (
|
|
||||||
<MediaMetaField
|
|
||||||
value={tagsContent}
|
|
||||||
title={1 < tagsContent.length ? translateString('Tags') : translateString('Tag')}
|
|
||||||
id="tags"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{categoriesContent.length ? (
|
|
||||||
<MediaMetaField
|
|
||||||
value={categoriesContent}
|
|
||||||
title={1 < categoriesContent.length ? translateString('Categories') : translateString('Category')}
|
|
||||||
id="categories"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{userCan.editMedia ? (
|
function onMediaDeleteFail(mediaId) {
|
||||||
<div className="media-author-actions">
|
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
|
||||||
{userCan.editMedia ? <EditMediaButton link={MediaPageStore.get('media-data').edit_url} /> : null}
|
setTimeout(function () {
|
||||||
|
PageActions.addNotification('Media removal failed', 'mediaDeleteFail');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
{userCan.deleteMedia ? (
|
if (void 0 !== mediaId) {
|
||||||
<PopupTrigger contentRef={popupContentRef}>
|
console.info('Media "' + mediaId + '"' + ' removal failed');
|
||||||
<button className="remove-media-icon" title={translateString('Delete media')}>
|
}
|
||||||
<i className="material-icons">delete</i>
|
}
|
||||||
</button>
|
|
||||||
</PopupTrigger>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{userCan.deleteMedia ? (
|
function onClickLoadMore() {
|
||||||
<PopupContent contentRef={popupContentRef}>
|
setIsContentVisible(!isContentVisible);
|
||||||
<PopupMain>
|
}
|
||||||
<div className="popup-message">
|
|
||||||
<span className="popup-message-title">Media removal</span>
|
useEffect(() => {
|
||||||
<span className="popup-message-main">You're willing to remove media permanently?</span>
|
MediaPageStore.on('media_delete', onMediaDelete);
|
||||||
</div>
|
MediaPageStore.on('media_delete_fail', onMediaDeleteFail);
|
||||||
<hr />
|
return () => {
|
||||||
<span className="popup-message-bottom">
|
MediaPageStore.removeListener('media_delete', onMediaDelete);
|
||||||
<button className="button-link cancel-comment-removal" onClick={cancelMediaRemoval}>
|
MediaPageStore.removeListener('media_delete_fail', onMediaDeleteFail);
|
||||||
CANCEL
|
};
|
||||||
</button>
|
}, []);
|
||||||
<button className="button-link proceed-comment-removal" onClick={proceedMediaRemoval}>
|
|
||||||
PROCEED
|
const authorLink = formatInnerLink(props.author.url, SiteContext._currentValue.url);
|
||||||
</button>
|
const authorThumb = formatInnerLink(props.author.thumb, SiteContext._currentValue.url);
|
||||||
</span>
|
|
||||||
</PopupMain>
|
function setTimestampAnchors(text) {
|
||||||
</PopupContent>
|
function wrapTimestampWithAnchor(match, string) {
|
||||||
) : null}
|
let split = match.split(':'),
|
||||||
|
s = 0,
|
||||||
|
m = 1;
|
||||||
|
|
||||||
|
while (split.length > 0) {
|
||||||
|
s += m * parseInt(split.pop(), 10);
|
||||||
|
m *= 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapped = `<a href="#" data-timestamp="${s}" class="video-timestamp">${match}</a>`;
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeRegex = new RegExp('((\\d)?\\d:)?(\\d)?\\d:\\d\\d', 'g');
|
||||||
|
return text.replace(timeRegex, wrapTimestampWithAnchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="media-info-content">
|
||||||
|
{void 0 === PageStore.get('config-media-item').displayAuthor ||
|
||||||
|
null === PageStore.get('config-media-item').displayAuthor ||
|
||||||
|
!!PageStore.get('config-media-item').displayAuthor ? (
|
||||||
|
<MediaAuthorBanner
|
||||||
|
link={authorLink}
|
||||||
|
thumb={authorThumb}
|
||||||
|
name={props.author.name}
|
||||||
|
published={props.published}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="media-content-banner">
|
||||||
|
<div className="media-content-banner-inner">
|
||||||
|
{hasSummary ? <div className="media-content-summary">{summary}</div> : null}
|
||||||
|
{(!hasSummary || isContentVisible) && description ? (
|
||||||
|
<div
|
||||||
|
className="media-content-description"
|
||||||
|
dangerouslySetInnerHTML={{ __html: setTimestampAnchors(description) }}
|
||||||
|
></div>
|
||||||
|
) : null}
|
||||||
|
{hasSummary ? (
|
||||||
|
<button className="load-more" onClick={onClickLoadMore}>
|
||||||
|
{isContentVisible ? 'SHOW LESS' : 'SHOW MORE'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{tagsContent.length ? (
|
||||||
|
<MediaMetaField
|
||||||
|
value={tagsContent}
|
||||||
|
title={1 < tagsContent.length ? translateString('Tags') : translateString('Tag')}
|
||||||
|
id="tags"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{categoriesContent.length ? (
|
||||||
|
<MediaMetaField
|
||||||
|
value={categoriesContent}
|
||||||
|
title={
|
||||||
|
1 < categoriesContent.length
|
||||||
|
? translateString('Categories')
|
||||||
|
: translateString('Category')
|
||||||
|
}
|
||||||
|
id="categories"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{userCan.editMedia ? (
|
||||||
|
<div className="media-author-actions">
|
||||||
|
{userCan.editMedia ? (
|
||||||
|
<EditMediaButton link={MediaPageStore.get('media-data').edit_url} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{userCan.deleteMedia ? (
|
||||||
|
<PopupTrigger contentRef={popupContentRef}>
|
||||||
|
<button className="remove-media-icon" title={translateString('Delete media')}>
|
||||||
|
<i className="material-icons">delete</i>
|
||||||
|
</button>
|
||||||
|
</PopupTrigger>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{userCan.deleteMedia ? (
|
||||||
|
<PopupContent contentRef={popupContentRef}>
|
||||||
|
<PopupMain>
|
||||||
|
<div className="popup-message">
|
||||||
|
<span className="popup-message-title">Media removal</span>
|
||||||
|
<span className="popup-message-main">
|
||||||
|
You're willing to remove media permanently?
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<span className="popup-message-bottom">
|
||||||
|
<button
|
||||||
|
className="button-link cancel-comment-removal"
|
||||||
|
onClick={cancelMediaRemoval}
|
||||||
|
>
|
||||||
|
CANCEL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="button-link proceed-comment-removal"
|
||||||
|
onClick={proceedMediaRemoval}
|
||||||
|
>
|
||||||
|
PROCEED
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</PopupMain>
|
||||||
|
</PopupContent>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CommentsList />
|
{!inEmbeddedApp() && <CommentsList />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,107 +1,119 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { formatViewsNumber } from '../../utils/helpers/';
|
import { formatViewsNumber, inEmbeddedApp } from '../../utils/helpers/';
|
||||||
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
||||||
import { MemberContext, PlaylistsContext } from '../../utils/contexts/';
|
import { MemberContext, PlaylistsContext } from '../../utils/contexts/';
|
||||||
import { MediaLikeIcon, MediaDislikeIcon, OtherMediaDownloadLink, VideoMediaDownloadLink, MediaSaveButton, MediaShareButton, MediaMoreOptionsIcon } from '../media-actions/';
|
import {
|
||||||
|
MediaLikeIcon,
|
||||||
|
MediaDislikeIcon,
|
||||||
|
OtherMediaDownloadLink,
|
||||||
|
VideoMediaDownloadLink,
|
||||||
|
MediaSaveButton,
|
||||||
|
MediaShareButton,
|
||||||
|
MediaMoreOptionsIcon,
|
||||||
|
} from '../media-actions/';
|
||||||
import ViewerInfoTitleBanner from './ViewerInfoTitleBanner';
|
import ViewerInfoTitleBanner from './ViewerInfoTitleBanner';
|
||||||
import { translateString } from '../../utils/helpers/';
|
import { translateString } from '../../utils/helpers/';
|
||||||
|
|
||||||
export default class ViewerInfoVideoTitleBanner extends ViewerInfoTitleBanner {
|
export default class ViewerInfoVideoTitleBanner extends ViewerInfoTitleBanner {
|
||||||
render() {
|
render() {
|
||||||
const displayViews = PageStore.get('config-options').pages.media.displayViews && void 0 !== this.props.views;
|
const displayViews = PageStore.get('config-options').pages.media.displayViews && void 0 !== this.props.views;
|
||||||
|
|
||||||
const mediaData = MediaPageStore.get('media-data');
|
const mediaData = MediaPageStore.get('media-data');
|
||||||
const mediaState = mediaData.state;
|
const mediaState = mediaData.state;
|
||||||
const isShared = mediaData.is_shared;
|
const isShared = mediaData.is_shared;
|
||||||
|
|
||||||
let stateTooltip = '';
|
let stateTooltip = '';
|
||||||
|
|
||||||
switch (mediaState) {
|
switch (mediaState) {
|
||||||
case 'private':
|
case 'private':
|
||||||
stateTooltip = 'The site admins have to make its access public';
|
stateTooltip = 'The site admins have to make its access public';
|
||||||
break;
|
break;
|
||||||
case 'unlisted':
|
case 'unlisted':
|
||||||
stateTooltip = 'The site admins have to make it appear on listings';
|
stateTooltip = 'The site admins have to make it appear on listings';
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedTooltip = 'This media is shared with specific users or categories';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="media-title-banner">
|
||||||
|
{displayViews && PageStore.get('config-options').pages.media.categoriesWithTitle
|
||||||
|
? this.mediaCategories(true)
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{void 0 !== this.props.title ? <h1>{this.props.title}</h1> : null}
|
||||||
|
|
||||||
|
{isShared || 'public' !== mediaState ? (
|
||||||
|
<div className="media-labels-area">
|
||||||
|
<div className="media-labels-area-inner">
|
||||||
|
{isShared ? (
|
||||||
|
<>
|
||||||
|
<span className="media-label-state">
|
||||||
|
<span>shared</span>
|
||||||
|
</span>
|
||||||
|
<span className="helper-icon" data-tooltip={sharedTooltip}>
|
||||||
|
<i className="material-icons">help_outline</i>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : 'public' !== mediaState ? (
|
||||||
|
<>
|
||||||
|
<span className="media-label-state">
|
||||||
|
<span>{mediaState}</span>
|
||||||
|
</span>
|
||||||
|
<span className="helper-icon" data-tooltip={stateTooltip}>
|
||||||
|
<i className="material-icons">help_outline</i>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'media-views-actions' +
|
||||||
|
(this.state.likedMedia ? ' liked-media' : '') +
|
||||||
|
(this.state.dislikedMedia ? ' disliked-media' : '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!displayViews && PageStore.get('config-options').pages.media.categoriesWithTitle
|
||||||
|
? this.mediaCategories()
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{displayViews ? (
|
||||||
|
<div className="media-views">
|
||||||
|
{formatViewsNumber(this.props.views, true)}{' '}
|
||||||
|
{1 >= this.props.views ? translateString('view') : translateString('views')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="media-actions">
|
||||||
|
<div>
|
||||||
|
{MemberContext._currentValue.can.likeMedia ? <MediaLikeIcon /> : null}
|
||||||
|
{MemberContext._currentValue.can.dislikeMedia ? <MediaDislikeIcon /> : null}
|
||||||
|
{!inEmbeddedApp() && MemberContext._currentValue.can.shareMedia ? (
|
||||||
|
<MediaShareButton isVideo={true} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!inEmbeddedApp() &&
|
||||||
|
!MemberContext._currentValue.is.anonymous &&
|
||||||
|
MemberContext._currentValue.can.saveMedia &&
|
||||||
|
-1 < PlaylistsContext._currentValue.mediaTypes.indexOf(MediaPageStore.get('media-type')) ? (
|
||||||
|
<MediaSaveButton />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!this.props.allowDownload || !MemberContext._currentValue.can.downloadMedia ? null : !this
|
||||||
|
.downloadLink ? (
|
||||||
|
<VideoMediaDownloadLink />
|
||||||
|
) : (
|
||||||
|
<OtherMediaDownloadLink link={this.downloadLink} title={this.downloadFilename} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MediaMoreOptionsIcon allowDownload={this.props.allowDownload} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharedTooltip = 'This media is shared with specific users or categories';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="media-title-banner">
|
|
||||||
{displayViews && PageStore.get('config-options').pages.media.categoriesWithTitle
|
|
||||||
? this.mediaCategories(true)
|
|
||||||
: null}
|
|
||||||
|
|
||||||
{void 0 !== this.props.title ? <h1>{this.props.title}</h1> : null}
|
|
||||||
|
|
||||||
{isShared || 'public' !== mediaState ? (
|
|
||||||
<div className="media-labels-area">
|
|
||||||
<div className="media-labels-area-inner">
|
|
||||||
{isShared ? (
|
|
||||||
<>
|
|
||||||
<span className="media-label-state">
|
|
||||||
<span>shared</span>
|
|
||||||
</span>
|
|
||||||
<span className="helper-icon" data-tooltip={sharedTooltip}>
|
|
||||||
<i className="material-icons">help_outline</i>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : 'public' !== mediaState ? (
|
|
||||||
<>
|
|
||||||
<span className="media-label-state">
|
|
||||||
<span>{mediaState}</span>
|
|
||||||
</span>
|
|
||||||
<span className="helper-icon" data-tooltip={stateTooltip}>
|
|
||||||
<i className="material-icons">help_outline</i>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'media-views-actions' +
|
|
||||||
(this.state.likedMedia ? ' liked-media' : '') +
|
|
||||||
(this.state.dislikedMedia ? ' disliked-media' : '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!displayViews && PageStore.get('config-options').pages.media.categoriesWithTitle
|
|
||||||
? this.mediaCategories()
|
|
||||||
: null}
|
|
||||||
|
|
||||||
{displayViews ? (
|
|
||||||
<div className="media-views">
|
|
||||||
{formatViewsNumber(this.props.views, true)} {1 >= this.props.views ? translateString('view') : translateString('views')}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="media-actions">
|
|
||||||
<div>
|
|
||||||
{MemberContext._currentValue.can.likeMedia ? <MediaLikeIcon /> : null}
|
|
||||||
{MemberContext._currentValue.can.dislikeMedia ? <MediaDislikeIcon /> : null}
|
|
||||||
{MemberContext._currentValue.can.shareMedia ? <MediaShareButton isVideo={true} /> : null}
|
|
||||||
|
|
||||||
{!MemberContext._currentValue.is.anonymous &&
|
|
||||||
MemberContext._currentValue.can.saveMedia &&
|
|
||||||
-1 < PlaylistsContext._currentValue.mediaTypes.indexOf(MediaPageStore.get('media-type')) ? (
|
|
||||||
<MediaSaveButton />
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!this.props.allowDownload || !MemberContext._currentValue.can.downloadMedia ? null : !this
|
|
||||||
.downloadLink ? (
|
|
||||||
<VideoMediaDownloadLink />
|
|
||||||
) : (
|
|
||||||
<OtherMediaDownloadLink link={this.downloadLink} title={this.downloadFilename} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<MediaMoreOptionsIcon allowDownload={this.props.allowDownload} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
@@ -1,28 +1,33 @@
|
|||||||
.page-main-wrap {
|
.page-main-wrap {
|
||||||
padding-top: var(--header-height);
|
padding-top: var(--header-height);
|
||||||
will-change: padding-left;
|
will-change: padding-left;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.visible-sidebar & {
|
||||||
|
padding-left: var(--sidebar-width);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible-sidebar #page-media & {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.visible-sidebar & {
|
.visible-sidebar & {
|
||||||
padding-left: var(--sidebar-width);
|
#page-media {
|
||||||
opacity: 1;
|
padding-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.visible-sidebar #page-media & {
|
body.sliding-sidebar & {
|
||||||
padding-left: 0;
|
transition-property: padding-left;
|
||||||
}
|
transition-duration: 0.2s;
|
||||||
|
|
||||||
.visible-sidebar & {
|
|
||||||
#page-media {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
body.sliding-sidebar & {
|
.embedded-app & {
|
||||||
transition-property: padding-left;
|
padding-top: 0;
|
||||||
transition-duration: 0.2s;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#page-profile-media,
|
#page-profile-media,
|
||||||
@@ -30,20 +35,20 @@
|
|||||||
#page-profile-about,
|
#page-profile-about,
|
||||||
#page-liked.profile-page-liked,
|
#page-liked.profile-page-liked,
|
||||||
#page-history.profile-page-history {
|
#page-history.profile-page-history {
|
||||||
.page-main {
|
.page-main {
|
||||||
min-height: calc(100vh - var(--header-height));
|
min-height: calc(100vh - var(--header-height));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-main {
|
.page-main {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-main-inner {
|
.page-main-inner {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 1em 1em 0 1em;
|
margin: 1em 1em 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#page-profile-media,
|
#page-profile-media,
|
||||||
@@ -51,7 +56,7 @@
|
|||||||
#page-profile-about,
|
#page-profile-about,
|
||||||
#page-liked.profile-page-liked,
|
#page-liked.profile-page-liked,
|
||||||
#page-history.profile-page-history {
|
#page-history.profile-page-history {
|
||||||
.page-main-wrap {
|
.page-main-wrap {
|
||||||
background-color: var(--body-bg-color);
|
background-color: var(--body-bg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import UrlParse from 'url-parse';
|
import UrlParse from 'url-parse';
|
||||||
import { ApiUrlContext, MemberContext, SiteContext } from '../utils/contexts/';
|
import { ApiUrlContext, MemberContext, SiteContext } from '../utils/contexts/';
|
||||||
import { formatInnerLink, csrfToken, postRequest } from '../utils/helpers/';
|
import { formatInnerLink, csrfToken, postRequest, inEmbeddedApp } from '../utils/helpers/';
|
||||||
import { PageActions } from '../utils/actions/';
|
import { PageActions } from '../utils/actions/';
|
||||||
import { PageStore, ProfilePageStore } from '../utils/stores/';
|
import { PageStore, ProfilePageStore } from '../utils/stores/';
|
||||||
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
||||||
@@ -268,7 +268,7 @@ export class ProfileAboutPage extends ProfileMediaPage {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
this.state.author ? (
|
this.state.author ? (
|
||||||
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="about" />
|
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="about" hideChannelBanner={inEmbeddedApp()} />
|
||||||
) : null,
|
) : null,
|
||||||
this.state.author ? (
|
this.state.author ? (
|
||||||
<ProfilePagesContent key="ProfilePagesContent" enabledContactForm={this.enabledContactForm}>
|
<ProfilePagesContent key="ProfilePagesContent" enabledContactForm={this.enabledContactForm}>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ApiUrlConsumer } from '../utils/contexts/';
|
import { ApiUrlConsumer } from '../utils/contexts/';
|
||||||
import { PageStore } from '../utils/stores/';
|
import { PageStore } from '../utils/stores/';
|
||||||
|
import { inEmbeddedApp } from '../utils/helpers/';
|
||||||
import { MediaListWrapper } from '../components/MediaListWrapper';
|
import { MediaListWrapper } from '../components/MediaListWrapper';
|
||||||
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
||||||
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
||||||
@@ -28,7 +29,7 @@ export class ProfileHistoryPage extends ProfileMediaPage {
|
|||||||
pageContent() {
|
pageContent() {
|
||||||
return [
|
return [
|
||||||
this.state.author ? (
|
this.state.author ? (
|
||||||
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="history" />
|
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="history" hideChannelBanner={inEmbeddedApp()} />
|
||||||
) : null,
|
) : null,
|
||||||
this.state.author ? (
|
this.state.author ? (
|
||||||
<ProfilePagesContent key="ProfilePagesContent">
|
<ProfilePagesContent key="ProfilePagesContent">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ApiUrlConsumer } from '../utils/contexts/';
|
import { ApiUrlConsumer } from '../utils/contexts/';
|
||||||
import { PageStore } from '../utils/stores/';
|
import { PageStore } from '../utils/stores/';
|
||||||
|
import { inEmbeddedApp } from '../utils/helpers/';
|
||||||
import { MediaListWrapper } from '../components/MediaListWrapper';
|
import { MediaListWrapper } from '../components/MediaListWrapper';
|
||||||
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
||||||
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
||||||
@@ -28,7 +29,7 @@ export class ProfileLikedPage extends ProfileMediaPage {
|
|||||||
pageContent() {
|
pageContent() {
|
||||||
return [
|
return [
|
||||||
this.state.author ? (
|
this.state.author ? (
|
||||||
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="liked" />
|
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="liked" hideChannelBanner={inEmbeddedApp()} />
|
||||||
) : null,
|
) : null,
|
||||||
this.state.author ? (
|
this.state.author ? (
|
||||||
<ProfilePagesContent key="ProfilePagesContent">
|
<ProfilePagesContent key="ProfilePagesContent">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ApiUrlConsumer } from '../utils/contexts/';
|
import { ApiUrlConsumer } from '../utils/contexts/';
|
||||||
import { PageStore } from '../utils/stores/';
|
import { PageStore } from '../utils/stores/';
|
||||||
|
import { inEmbeddedApp } from '../utils/helpers/';
|
||||||
import { MediaListWrapper } from '../components/MediaListWrapper';
|
import { MediaListWrapper } from '../components/MediaListWrapper';
|
||||||
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
||||||
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
||||||
@@ -30,7 +31,7 @@ export class ProfilePlaylistsPage extends ProfileMediaPage {
|
|||||||
pageContent() {
|
pageContent() {
|
||||||
return [
|
return [
|
||||||
this.state.author ? (
|
this.state.author ? (
|
||||||
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="playlists" />
|
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="playlists" hideChannelBanner={inEmbeddedApp()} />
|
||||||
) : null,
|
) : null,
|
||||||
this.state.author ? (
|
this.state.author ? (
|
||||||
<ProfilePagesContent key="ProfilePagesContent">
|
<ProfilePagesContent key="ProfilePagesContent">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFi
|
|||||||
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
|
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
|
||||||
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
|
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
|
||||||
import { BulkActionsModals } from '../components/BulkActionsModals';
|
import { BulkActionsModals } from '../components/BulkActionsModals';
|
||||||
import { translateString } from '../utils/helpers';
|
import { inEmbeddedApp, translateString } from '../utils/helpers';
|
||||||
import { withBulkActions } from '../utils/hoc/withBulkActions';
|
import { withBulkActions } from '../utils/hoc/withBulkActions';
|
||||||
|
|
||||||
import { Page } from './_Page';
|
import { Page } from './_Page';
|
||||||
@@ -19,400 +19,443 @@ import { Page } from './_Page';
|
|||||||
import '../components/profile-page/ProfilePage.scss';
|
import '../components/profile-page/ProfilePage.scss';
|
||||||
|
|
||||||
function EmptySharedByMe(props) {
|
function EmptySharedByMe(props) {
|
||||||
return (
|
return (
|
||||||
<LinksConsumer>
|
<LinksConsumer>
|
||||||
{(links) => (
|
{(links) => (
|
||||||
<div className="empty-media empty-channel-media">
|
<div className="empty-media empty-channel-media">
|
||||||
<div className="welcome-title">No shared media</div>
|
<div className="welcome-title">No shared media</div>
|
||||||
<div className="start-uploading">
|
<div className="start-uploading">Media that you have shared with others will show up here.</div>
|
||||||
Media that you have shared with others will show up here.
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</LinksConsumer>
|
||||||
)}
|
);
|
||||||
</LinksConsumer>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProfileSharedByMePage extends Page {
|
class ProfileSharedByMePage extends Page {
|
||||||
constructor(props, pageSlug) {
|
constructor(props, pageSlug) {
|
||||||
super(props, 'string' === typeof pageSlug ? pageSlug : 'author-shared-by-me');
|
super(props, 'string' === typeof pageSlug ? pageSlug : 'author-shared-by-me');
|
||||||
|
|
||||||
this.profilePageSlug = 'string' === typeof pageSlug ? pageSlug : 'author-shared-by-me';
|
this.profilePageSlug = 'string' === typeof pageSlug ? pageSlug : 'author-shared-by-me';
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
channelMediaCount: -1,
|
channelMediaCount: -1,
|
||||||
author: ProfilePageStore.get('author-data'),
|
author: ProfilePageStore.get('author-data'),
|
||||||
uploadsPreviewItemsCount: 0,
|
uploadsPreviewItemsCount: 0,
|
||||||
title: this.props.title,
|
title: this.props.title,
|
||||||
query: ProfilePageStore.get('author-query'),
|
query: ProfilePageStore.get('author-query'),
|
||||||
requestUrl: null,
|
requestUrl: null,
|
||||||
hiddenFilters: true,
|
hiddenFilters: true,
|
||||||
hiddenTags: true,
|
hiddenTags: true,
|
||||||
hiddenSorting: true,
|
hiddenSorting: true,
|
||||||
filterArgs: '',
|
filterArgs: '',
|
||||||
availableTags: [],
|
availableTags: [],
|
||||||
selectedTag: 'all',
|
selectedTag: 'all',
|
||||||
selectedSort: 'date_added_desc',
|
selectedSort: 'date_added_desc',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.authorDataLoad = this.authorDataLoad.bind(this);
|
this.authorDataLoad = this.authorDataLoad.bind(this);
|
||||||
this.onAuthorPreviewItemsCountCallback = this.onAuthorPreviewItemsCountCallback.bind(this);
|
this.onAuthorPreviewItemsCountCallback = this.onAuthorPreviewItemsCountCallback.bind(this);
|
||||||
this.getCountFunc = this.getCountFunc.bind(this);
|
this.getCountFunc = this.getCountFunc.bind(this);
|
||||||
this.changeRequestQuery = this.changeRequestQuery.bind(this);
|
this.changeRequestQuery = this.changeRequestQuery.bind(this);
|
||||||
this.onToggleFiltersClick = this.onToggleFiltersClick.bind(this);
|
this.onToggleFiltersClick = this.onToggleFiltersClick.bind(this);
|
||||||
this.onToggleTagsClick = this.onToggleTagsClick.bind(this);
|
this.onToggleTagsClick = this.onToggleTagsClick.bind(this);
|
||||||
this.onToggleSortingClick = this.onToggleSortingClick.bind(this);
|
this.onToggleSortingClick = this.onToggleSortingClick.bind(this);
|
||||||
this.onFiltersUpdate = this.onFiltersUpdate.bind(this);
|
this.onFiltersUpdate = this.onFiltersUpdate.bind(this);
|
||||||
this.onTagSelect = this.onTagSelect.bind(this);
|
this.onTagSelect = this.onTagSelect.bind(this);
|
||||||
this.onSortSelect = this.onSortSelect.bind(this);
|
this.onSortSelect = this.onSortSelect.bind(this);
|
||||||
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
|
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
|
||||||
|
|
||||||
ProfilePageStore.on('load-author-data', this.authorDataLoad);
|
ProfilePageStore.on('load-author-data', this.authorDataLoad);
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
ProfilePageActions.load_author_data();
|
|
||||||
}
|
|
||||||
|
|
||||||
authorDataLoad() {
|
|
||||||
const author = ProfilePageStore.get('author-data');
|
|
||||||
|
|
||||||
let requestUrl = this.state.requestUrl;
|
|
||||||
|
|
||||||
if (author) {
|
|
||||||
if (this.state.query) {
|
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_by_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs;
|
|
||||||
} else {
|
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_by_me' + this.state.filterArgs;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
componentDidMount() {
|
||||||
author: author,
|
ProfilePageActions.load_author_data();
|
||||||
requestUrl: requestUrl,
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAuthorPreviewItemsCountCallback(totalAuthorPreviewItems) {
|
authorDataLoad() {
|
||||||
this.setState({
|
const author = ProfilePageStore.get('author-data');
|
||||||
uploadsPreviewItemsCount: totalAuthorPreviewItems,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getCountFunc(count) {
|
let requestUrl = this.state.requestUrl;
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
channelMediaCount: count,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
if (this.state.query) {
|
|
||||||
let title = '';
|
|
||||||
|
|
||||||
if (!count) {
|
if (author) {
|
||||||
title = 'No results for "' + this.state.query + '"';
|
if (this.state.query) {
|
||||||
} else if (1 === count) {
|
requestUrl =
|
||||||
title = '1 result for "' + this.state.query + '"';
|
ApiUrlContext._currentValue.media +
|
||||||
} else {
|
'?author=' +
|
||||||
title = count + ' results for "' + this.state.query + '"';
|
author.id +
|
||||||
}
|
'&show=shared_by_me&q=' +
|
||||||
|
encodeURIComponent(this.state.query) +
|
||||||
this.setState({
|
this.state.filterArgs;
|
||||||
title: title,
|
} else {
|
||||||
});
|
requestUrl =
|
||||||
|
ApiUrlContext._currentValue.media +
|
||||||
|
'?author=' +
|
||||||
|
author.id +
|
||||||
|
'&show=shared_by_me' +
|
||||||
|
this.state.filterArgs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeRequestQuery(newQuery) {
|
this.setState({
|
||||||
if (!this.state.author) {
|
author: author,
|
||||||
return;
|
requestUrl: requestUrl,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let requestUrl;
|
onAuthorPreviewItemsCountCallback(totalAuthorPreviewItems) {
|
||||||
|
this.setState({
|
||||||
if (newQuery) {
|
uploadsPreviewItemsCount: totalAuthorPreviewItems,
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me&q=' + encodeURIComponent(newQuery) + this.state.filterArgs;
|
});
|
||||||
} else {
|
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me' + this.state.filterArgs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = this.state.title;
|
getCountFunc(count) {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
channelMediaCount: count,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (this.state.query) {
|
||||||
|
let title = '';
|
||||||
|
|
||||||
if ('' === newQuery) {
|
if (!count) {
|
||||||
title = this.props.title;
|
title = 'No results for "' + this.state.query + '"';
|
||||||
|
} else if (1 === count) {
|
||||||
|
title = '1 result for "' + this.state.query + '"';
|
||||||
|
} else {
|
||||||
|
title = count + ' results for "' + this.state.query + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
title: title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
changeRequestQuery(newQuery) {
|
||||||
requestUrl: requestUrl,
|
|
||||||
query: newQuery,
|
|
||||||
title: title,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleFiltersClick() {
|
|
||||||
this.setState({
|
|
||||||
hiddenFilters: !this.state.hiddenFilters,
|
|
||||||
hiddenTags: true,
|
|
||||||
hiddenSorting: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleTagsClick() {
|
|
||||||
this.setState({
|
|
||||||
hiddenFilters: true,
|
|
||||||
hiddenTags: !this.state.hiddenTags,
|
|
||||||
hiddenSorting: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleSortingClick() {
|
|
||||||
this.setState({
|
|
||||||
hiddenFilters: true,
|
|
||||||
hiddenTags: true,
|
|
||||||
hiddenSorting: !this.state.hiddenSorting,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onTagSelect(tag) {
|
|
||||||
this.setState({ selectedTag: tag }, () => {
|
|
||||||
this.onFiltersUpdate({
|
|
||||||
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
|
|
||||||
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
|
|
||||||
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
|
|
||||||
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
|
|
||||||
sort_by: this.state.selectedSort,
|
|
||||||
tag: tag,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSortSelect(sortBy) {
|
|
||||||
this.setState({ selectedSort: sortBy }, () => {
|
|
||||||
this.onFiltersUpdate({
|
|
||||||
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
|
|
||||||
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
|
|
||||||
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
|
|
||||||
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
|
|
||||||
sort_by: sortBy,
|
|
||||||
tag: this.state.selectedTag,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onFiltersUpdate(updatedArgs) {
|
|
||||||
const args = {
|
|
||||||
media_type: null,
|
|
||||||
upload_date: null,
|
|
||||||
duration: null,
|
|
||||||
publish_state: null,
|
|
||||||
sort_by: null,
|
|
||||||
ordering: null,
|
|
||||||
t: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (updatedArgs.media_type) {
|
|
||||||
case 'video':
|
|
||||||
case 'audio':
|
|
||||||
case 'image':
|
|
||||||
case 'pdf':
|
|
||||||
args.media_type = updatedArgs.media_type;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (updatedArgs.upload_date) {
|
|
||||||
case 'today':
|
|
||||||
case 'this_week':
|
|
||||||
case 'this_month':
|
|
||||||
case 'this_year':
|
|
||||||
args.upload_date = updatedArgs.upload_date;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle duration filter
|
|
||||||
if (updatedArgs.duration && updatedArgs.duration !== 'all') {
|
|
||||||
args.duration = updatedArgs.duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle publish state filter
|
|
||||||
if (updatedArgs.publish_state && updatedArgs.publish_state !== 'all') {
|
|
||||||
args.publish_state = updatedArgs.publish_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (updatedArgs.sort_by) {
|
|
||||||
case 'date_added_desc':
|
|
||||||
// Default sorting, no need to add parameters
|
|
||||||
break;
|
|
||||||
case 'date_added_asc':
|
|
||||||
args.ordering = 'asc';
|
|
||||||
break;
|
|
||||||
case 'alphabetically_asc':
|
|
||||||
args.sort_by = 'title_asc';
|
|
||||||
break;
|
|
||||||
case 'alphabetically_desc':
|
|
||||||
args.sort_by = 'title_desc';
|
|
||||||
break;
|
|
||||||
case 'plays_least':
|
|
||||||
args.sort_by = 'views_asc';
|
|
||||||
break;
|
|
||||||
case 'plays_most':
|
|
||||||
args.sort_by = 'views_desc';
|
|
||||||
break;
|
|
||||||
case 'likes_least':
|
|
||||||
args.sort_by = 'likes_asc';
|
|
||||||
break;
|
|
||||||
case 'likes_most':
|
|
||||||
args.sort_by = 'likes_desc';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedArgs.tag && updatedArgs.tag !== 'all') {
|
|
||||||
args.t = updatedArgs.tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newArgs = [];
|
|
||||||
|
|
||||||
for (let arg in args) {
|
|
||||||
if (null !== args[arg]) {
|
|
||||||
newArgs.push(arg + '=' + args[arg]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
filterArgs: newArgs.length ? '&' + newArgs.join('&') : '',
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
if (!this.state.author) {
|
if (!this.state.author) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let requestUrl;
|
let requestUrl;
|
||||||
|
|
||||||
if (this.state.query) {
|
if (newQuery) {
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs;
|
requestUrl =
|
||||||
|
ApiUrlContext._currentValue.media +
|
||||||
|
'?author=' +
|
||||||
|
this.state.author.id +
|
||||||
|
'&show=shared_by_me&q=' +
|
||||||
|
encodeURIComponent(newQuery) +
|
||||||
|
this.state.filterArgs;
|
||||||
} else {
|
} else {
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me' + this.state.filterArgs;
|
requestUrl =
|
||||||
|
ApiUrlContext._currentValue.media +
|
||||||
|
'?author=' +
|
||||||
|
this.state.author.id +
|
||||||
|
'&show=shared_by_me' +
|
||||||
|
this.state.filterArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = this.state.title;
|
||||||
|
|
||||||
|
if ('' === newQuery) {
|
||||||
|
title = this.props.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
requestUrl: requestUrl,
|
requestUrl: requestUrl,
|
||||||
|
query: newQuery,
|
||||||
|
title: title,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onResponseDataLoaded(responseData) {
|
|
||||||
if (responseData && responseData.tags) {
|
|
||||||
const tags = responseData.tags.split(',').map((tag) => tag.trim()).filter((tag) => tag);
|
|
||||||
this.setState({ availableTags: tags });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pageContent() {
|
onToggleFiltersClick() {
|
||||||
const authorData = ProfilePageStore.get('author-data');
|
this.setState({
|
||||||
|
hiddenFilters: !this.state.hiddenFilters,
|
||||||
|
hiddenTags: true,
|
||||||
|
hiddenSorting: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
|
onToggleTagsClick() {
|
||||||
|
this.setState({
|
||||||
|
hiddenFilters: true,
|
||||||
|
hiddenTags: !this.state.hiddenTags,
|
||||||
|
hiddenSorting: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any filters are active
|
onToggleSortingClick() {
|
||||||
const hasActiveFilters = this.state.filterArgs && (
|
this.setState({
|
||||||
this.state.filterArgs.includes('media_type=') ||
|
hiddenFilters: true,
|
||||||
this.state.filterArgs.includes('upload_date=') ||
|
hiddenTags: true,
|
||||||
this.state.filterArgs.includes('duration=') ||
|
hiddenSorting: !this.state.hiddenSorting,
|
||||||
this.state.filterArgs.includes('publish_state=')
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
return [
|
onTagSelect(tag) {
|
||||||
this.state.author ? (
|
this.setState({ selectedTag: tag }, () => {
|
||||||
<ProfilePagesHeader
|
this.onFiltersUpdate({
|
||||||
key="ProfilePagesHeader"
|
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
|
||||||
author={this.state.author}
|
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
|
||||||
type="shared_by_me"
|
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
|
||||||
onQueryChange={this.changeRequestQuery}
|
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
|
||||||
onToggleFiltersClick={this.onToggleFiltersClick}
|
sort_by: this.state.selectedSort,
|
||||||
onToggleTagsClick={this.onToggleTagsClick}
|
tag: tag,
|
||||||
onToggleSortingClick={this.onToggleSortingClick}
|
});
|
||||||
hasActiveFilters={hasActiveFilters}
|
});
|
||||||
hasActiveTags={this.state.selectedTag !== 'all'}
|
}
|
||||||
hasActiveSort={this.state.selectedSort !== 'date_added_desc'}
|
|
||||||
/>
|
onSortSelect(sortBy) {
|
||||||
) : null,
|
this.setState({ selectedSort: sortBy }, () => {
|
||||||
this.state.author ? (
|
this.onFiltersUpdate({
|
||||||
<ProfilePagesContent key="ProfilePagesContent">
|
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
|
||||||
<MediaListWrapper
|
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
|
||||||
title={this.state.title}
|
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
|
||||||
className="items-list-ver"
|
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
|
||||||
showBulkActions={isMediaAuthor}
|
sort_by: sortBy,
|
||||||
selectedCount={this.props.bulkActions.selectedMedia.size}
|
tag: this.state.selectedTag,
|
||||||
totalCount={this.props.bulkActions.availableMediaIds.length}
|
});
|
||||||
onBulkAction={this.props.bulkActions.handleBulkAction}
|
});
|
||||||
onSelectAll={this.props.bulkActions.handleSelectAll}
|
}
|
||||||
onDeselectAll={this.props.bulkActions.handleDeselectAll}
|
|
||||||
>
|
onFiltersUpdate(updatedArgs) {
|
||||||
<ProfileMediaFilters hidden={this.state.hiddenFilters} tags={this.state.availableTags} onFiltersUpdate={this.onFiltersUpdate} />
|
const args = {
|
||||||
<ProfileMediaTags hidden={this.state.hiddenTags} tags={this.state.availableTags} onTagSelect={this.onTagSelect} />
|
media_type: null,
|
||||||
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
|
upload_date: null,
|
||||||
<LazyLoadItemListAsync
|
duration: null,
|
||||||
key={`${this.state.requestUrl}-${this.props.bulkActions.listKey}`}
|
publish_state: null,
|
||||||
requestUrl={this.state.requestUrl}
|
sort_by: null,
|
||||||
hideAuthor={true}
|
ordering: null,
|
||||||
itemsCountCallback={this.state.requestUrl ? this.getCountFunc : null}
|
t: null,
|
||||||
hideViews={!PageStore.get('config-media-item').displayViews}
|
};
|
||||||
hideDate={!PageStore.get('config-media-item').displayPublishDate}
|
|
||||||
canEdit={isMediaAuthor}
|
switch (updatedArgs.media_type) {
|
||||||
onResponseDataLoaded={this.onResponseDataLoaded}
|
case 'video':
|
||||||
showSelection={isMediaAuthor}
|
case 'audio':
|
||||||
hasAnySelection={this.props.bulkActions.selectedMedia.size > 0}
|
case 'image':
|
||||||
selectedMedia={this.props.bulkActions.selectedMedia}
|
case 'pdf':
|
||||||
onMediaSelection={this.props.bulkActions.handleMediaSelection}
|
args.media_type = updatedArgs.media_type;
|
||||||
onItemsUpdate={this.props.bulkActions.handleItemsUpdate}
|
break;
|
||||||
/>
|
}
|
||||||
{isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
|
|
||||||
<EmptySharedByMe name={this.state.author.name} />
|
switch (updatedArgs.upload_date) {
|
||||||
) : null}
|
case 'today':
|
||||||
</MediaListWrapper>
|
case 'this_week':
|
||||||
</ProfilePagesContent>
|
case 'this_month':
|
||||||
) : null,
|
case 'this_year':
|
||||||
this.state.author && isMediaAuthor ? (
|
args.upload_date = updatedArgs.upload_date;
|
||||||
<BulkActionsModals
|
break;
|
||||||
key="BulkActionsModals"
|
}
|
||||||
{...this.props.bulkActions}
|
|
||||||
selectedMediaIds={Array.from(this.props.bulkActions.selectedMedia)}
|
// Handle duration filter
|
||||||
csrfToken={this.props.bulkActions.getCsrfToken()}
|
if (updatedArgs.duration && updatedArgs.duration !== 'all') {
|
||||||
username={this.state.author.username}
|
args.duration = updatedArgs.duration;
|
||||||
onConfirmCancel={this.props.bulkActions.handleConfirmCancel}
|
}
|
||||||
onConfirmProceed={this.props.bulkActions.handleConfirmProceed}
|
|
||||||
onPermissionModalCancel={this.props.bulkActions.handlePermissionModalCancel}
|
// Handle publish state filter
|
||||||
onPermissionModalSuccess={this.props.bulkActions.handlePermissionModalSuccess}
|
if (updatedArgs.publish_state && updatedArgs.publish_state !== 'all') {
|
||||||
onPermissionModalError={this.props.bulkActions.handlePermissionModalError}
|
args.publish_state = updatedArgs.publish_state;
|
||||||
onPlaylistModalCancel={this.props.bulkActions.handlePlaylistModalCancel}
|
}
|
||||||
onPlaylistModalSuccess={this.props.bulkActions.handlePlaylistModalSuccess}
|
|
||||||
onPlaylistModalError={this.props.bulkActions.handlePlaylistModalError}
|
switch (updatedArgs.sort_by) {
|
||||||
onChangeOwnerModalCancel={this.props.bulkActions.handleChangeOwnerModalCancel}
|
case 'date_added_desc':
|
||||||
onChangeOwnerModalSuccess={this.props.bulkActions.handleChangeOwnerModalSuccess}
|
// Default sorting, no need to add parameters
|
||||||
onChangeOwnerModalError={this.props.bulkActions.handleChangeOwnerModalError}
|
break;
|
||||||
onPublishStateModalCancel={this.props.bulkActions.handlePublishStateModalCancel}
|
case 'date_added_asc':
|
||||||
onPublishStateModalSuccess={this.props.bulkActions.handlePublishStateModalSuccess}
|
args.ordering = 'asc';
|
||||||
onPublishStateModalError={this.props.bulkActions.handlePublishStateModalError}
|
break;
|
||||||
onCategoryModalCancel={this.props.bulkActions.handleCategoryModalCancel}
|
case 'alphabetically_asc':
|
||||||
onCategoryModalSuccess={this.props.bulkActions.handleCategoryModalSuccess}
|
args.sort_by = 'title_asc';
|
||||||
onCategoryModalError={this.props.bulkActions.handleCategoryModalError}
|
break;
|
||||||
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
|
case 'alphabetically_desc':
|
||||||
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
|
args.sort_by = 'title_desc';
|
||||||
onTagModalError={this.props.bulkActions.handleTagModalError}
|
break;
|
||||||
/>
|
case 'plays_least':
|
||||||
) : null,
|
args.sort_by = 'views_asc';
|
||||||
];
|
break;
|
||||||
}
|
case 'plays_most':
|
||||||
|
args.sort_by = 'views_desc';
|
||||||
|
break;
|
||||||
|
case 'likes_least':
|
||||||
|
args.sort_by = 'likes_asc';
|
||||||
|
break;
|
||||||
|
case 'likes_most':
|
||||||
|
args.sort_by = 'likes_desc';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedArgs.tag && updatedArgs.tag !== 'all') {
|
||||||
|
args.t = updatedArgs.tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newArgs = [];
|
||||||
|
|
||||||
|
for (let arg in args) {
|
||||||
|
if (null !== args[arg]) {
|
||||||
|
newArgs.push(arg + '=' + args[arg]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
filterArgs: newArgs.length ? '&' + newArgs.join('&') : '',
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
if (!this.state.author) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let requestUrl;
|
||||||
|
|
||||||
|
if (this.state.query) {
|
||||||
|
requestUrl =
|
||||||
|
ApiUrlContext._currentValue.media +
|
||||||
|
'?author=' +
|
||||||
|
this.state.author.id +
|
||||||
|
'&show=shared_by_me&q=' +
|
||||||
|
encodeURIComponent(this.state.query) +
|
||||||
|
this.state.filterArgs;
|
||||||
|
} else {
|
||||||
|
requestUrl =
|
||||||
|
ApiUrlContext._currentValue.media +
|
||||||
|
'?author=' +
|
||||||
|
this.state.author.id +
|
||||||
|
'&show=shared_by_me' +
|
||||||
|
this.state.filterArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
requestUrl: requestUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onResponseDataLoaded(responseData) {
|
||||||
|
if (responseData && responseData.tags) {
|
||||||
|
const tags = responseData.tags
|
||||||
|
.split(',')
|
||||||
|
.map((tag) => tag.trim())
|
||||||
|
.filter((tag) => tag);
|
||||||
|
this.setState({ availableTags: tags });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageContent() {
|
||||||
|
const authorData = ProfilePageStore.get('author-data');
|
||||||
|
|
||||||
|
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
|
||||||
|
|
||||||
|
// Check if any filters are active
|
||||||
|
const hasActiveFilters =
|
||||||
|
this.state.filterArgs &&
|
||||||
|
(this.state.filterArgs.includes('media_type=') ||
|
||||||
|
this.state.filterArgs.includes('upload_date=') ||
|
||||||
|
this.state.filterArgs.includes('duration=') ||
|
||||||
|
this.state.filterArgs.includes('publish_state='));
|
||||||
|
|
||||||
|
return [
|
||||||
|
this.state.author ? (
|
||||||
|
<ProfilePagesHeader
|
||||||
|
key="ProfilePagesHeader"
|
||||||
|
author={this.state.author}
|
||||||
|
type="shared_by_me"
|
||||||
|
onQueryChange={this.changeRequestQuery}
|
||||||
|
onToggleFiltersClick={this.onToggleFiltersClick}
|
||||||
|
onToggleTagsClick={this.onToggleTagsClick}
|
||||||
|
onToggleSortingClick={this.onToggleSortingClick}
|
||||||
|
hasActiveFilters={hasActiveFilters}
|
||||||
|
hasActiveTags={this.state.selectedTag !== 'all'}
|
||||||
|
hasActiveSort={this.state.selectedSort !== 'date_added_desc'}
|
||||||
|
hideChannelBanner={inEmbeddedApp()}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
this.state.author ? (
|
||||||
|
<ProfilePagesContent key="ProfilePagesContent">
|
||||||
|
<MediaListWrapper
|
||||||
|
title={this.state.title}
|
||||||
|
className="items-list-ver"
|
||||||
|
showBulkActions={isMediaAuthor}
|
||||||
|
selectedCount={this.props.bulkActions.selectedMedia.size}
|
||||||
|
totalCount={this.props.bulkActions.availableMediaIds.length}
|
||||||
|
onBulkAction={this.props.bulkActions.handleBulkAction}
|
||||||
|
onSelectAll={this.props.bulkActions.handleSelectAll}
|
||||||
|
onDeselectAll={this.props.bulkActions.handleDeselectAll}
|
||||||
|
>
|
||||||
|
<ProfileMediaFilters
|
||||||
|
hidden={this.state.hiddenFilters}
|
||||||
|
tags={this.state.availableTags}
|
||||||
|
onFiltersUpdate={this.onFiltersUpdate}
|
||||||
|
/>
|
||||||
|
<ProfileMediaTags
|
||||||
|
hidden={this.state.hiddenTags}
|
||||||
|
tags={this.state.availableTags}
|
||||||
|
onTagSelect={this.onTagSelect}
|
||||||
|
/>
|
||||||
|
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
|
||||||
|
<LazyLoadItemListAsync
|
||||||
|
key={`${this.state.requestUrl}-${this.props.bulkActions.listKey}`}
|
||||||
|
requestUrl={this.state.requestUrl}
|
||||||
|
hideAuthor={true}
|
||||||
|
itemsCountCallback={this.state.requestUrl ? this.getCountFunc : null}
|
||||||
|
hideViews={!PageStore.get('config-media-item').displayViews}
|
||||||
|
hideDate={!PageStore.get('config-media-item').displayPublishDate}
|
||||||
|
canEdit={isMediaAuthor}
|
||||||
|
onResponseDataLoaded={this.onResponseDataLoaded}
|
||||||
|
showSelection={isMediaAuthor}
|
||||||
|
hasAnySelection={this.props.bulkActions.selectedMedia.size > 0}
|
||||||
|
selectedMedia={this.props.bulkActions.selectedMedia}
|
||||||
|
onMediaSelection={this.props.bulkActions.handleMediaSelection}
|
||||||
|
onItemsUpdate={this.props.bulkActions.handleItemsUpdate}
|
||||||
|
/>
|
||||||
|
{isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
|
||||||
|
<EmptySharedByMe name={this.state.author.name} />
|
||||||
|
) : null}
|
||||||
|
</MediaListWrapper>
|
||||||
|
</ProfilePagesContent>
|
||||||
|
) : null,
|
||||||
|
this.state.author && isMediaAuthor ? (
|
||||||
|
<BulkActionsModals
|
||||||
|
key="BulkActionsModals"
|
||||||
|
{...this.props.bulkActions}
|
||||||
|
selectedMediaIds={Array.from(this.props.bulkActions.selectedMedia)}
|
||||||
|
csrfToken={this.props.bulkActions.getCsrfToken()}
|
||||||
|
username={this.state.author.username}
|
||||||
|
onConfirmCancel={this.props.bulkActions.handleConfirmCancel}
|
||||||
|
onConfirmProceed={this.props.bulkActions.handleConfirmProceed}
|
||||||
|
onPermissionModalCancel={this.props.bulkActions.handlePermissionModalCancel}
|
||||||
|
onPermissionModalSuccess={this.props.bulkActions.handlePermissionModalSuccess}
|
||||||
|
onPermissionModalError={this.props.bulkActions.handlePermissionModalError}
|
||||||
|
onPlaylistModalCancel={this.props.bulkActions.handlePlaylistModalCancel}
|
||||||
|
onPlaylistModalSuccess={this.props.bulkActions.handlePlaylistModalSuccess}
|
||||||
|
onPlaylistModalError={this.props.bulkActions.handlePlaylistModalError}
|
||||||
|
onChangeOwnerModalCancel={this.props.bulkActions.handleChangeOwnerModalCancel}
|
||||||
|
onChangeOwnerModalSuccess={this.props.bulkActions.handleChangeOwnerModalSuccess}
|
||||||
|
onChangeOwnerModalError={this.props.bulkActions.handleChangeOwnerModalError}
|
||||||
|
onPublishStateModalCancel={this.props.bulkActions.handlePublishStateModalCancel}
|
||||||
|
onPublishStateModalSuccess={this.props.bulkActions.handlePublishStateModalSuccess}
|
||||||
|
onPublishStateModalError={this.props.bulkActions.handlePublishStateModalError}
|
||||||
|
onCategoryModalCancel={this.props.bulkActions.handleCategoryModalCancel}
|
||||||
|
onCategoryModalSuccess={this.props.bulkActions.handleCategoryModalSuccess}
|
||||||
|
onCategoryModalError={this.props.bulkActions.handleCategoryModalError}
|
||||||
|
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
|
||||||
|
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
|
||||||
|
onTagModalError={this.props.bulkActions.handleTagModalError}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfileSharedByMePage.propTypes = {
|
ProfileSharedByMePage.propTypes = {
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
bulkActions: PropTypes.object.isRequired,
|
bulkActions: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
ProfileSharedByMePage.defaultProps = {
|
ProfileSharedByMePage.defaultProps = {
|
||||||
title: 'Shared by me',
|
title: 'Shared by me',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wrap with HOC and export as named export for compatibility
|
// Wrap with HOC and export as named export for compatibility
|
||||||
|
|||||||
@@ -10,364 +10,404 @@ import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListA
|
|||||||
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
|
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
|
||||||
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
|
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
|
||||||
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
|
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
|
||||||
import { translateString } from '../utils/helpers';
|
import { inEmbeddedApp, translateString } from '../utils/helpers';
|
||||||
|
|
||||||
import { Page } from './_Page';
|
import { Page } from './_Page';
|
||||||
|
|
||||||
import '../components/profile-page/ProfilePage.scss';
|
import '../components/profile-page/ProfilePage.scss';
|
||||||
|
|
||||||
function EmptySharedWithMe(props) {
|
function EmptySharedWithMe(props) {
|
||||||
return (
|
return (
|
||||||
<LinksConsumer>
|
<LinksConsumer>
|
||||||
{(links) => (
|
{(links) => (
|
||||||
<div className="empty-media empty-channel-media">
|
<div className="empty-media empty-channel-media">
|
||||||
<div className="welcome-title">No shared media</div>
|
<div className="welcome-title">No shared media</div>
|
||||||
<div className="start-uploading">
|
<div className="start-uploading">Media that others have shared with you will show up here.</div>
|
||||||
Media that others have shared with you will show up here.
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</LinksConsumer>
|
||||||
)}
|
);
|
||||||
</LinksConsumer>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProfileSharedWithMePage extends Page {
|
export class ProfileSharedWithMePage extends Page {
|
||||||
constructor(props, pageSlug) {
|
constructor(props, pageSlug) {
|
||||||
super(props, 'string' === typeof pageSlug ? pageSlug : 'author-shared-with-me');
|
super(props, 'string' === typeof pageSlug ? pageSlug : 'author-shared-with-me');
|
||||||
|
|
||||||
this.profilePageSlug = 'string' === typeof pageSlug ? pageSlug : 'author-shared-with-me';
|
this.profilePageSlug = 'string' === typeof pageSlug ? pageSlug : 'author-shared-with-me';
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
channelMediaCount: -1,
|
channelMediaCount: -1,
|
||||||
author: ProfilePageStore.get('author-data'),
|
author: ProfilePageStore.get('author-data'),
|
||||||
uploadsPreviewItemsCount: 0,
|
uploadsPreviewItemsCount: 0,
|
||||||
title: this.props.title,
|
title: this.props.title,
|
||||||
query: ProfilePageStore.get('author-query'),
|
query: ProfilePageStore.get('author-query'),
|
||||||
requestUrl: null,
|
requestUrl: null,
|
||||||
hiddenFilters: true,
|
hiddenFilters: true,
|
||||||
hiddenTags: true,
|
hiddenTags: true,
|
||||||
hiddenSorting: true,
|
hiddenSorting: true,
|
||||||
filterArgs: '',
|
filterArgs: '',
|
||||||
availableTags: [],
|
availableTags: [],
|
||||||
selectedTag: 'all',
|
selectedTag: 'all',
|
||||||
selectedSort: 'date_added_desc',
|
selectedSort: 'date_added_desc',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.authorDataLoad = this.authorDataLoad.bind(this);
|
this.authorDataLoad = this.authorDataLoad.bind(this);
|
||||||
this.onAuthorPreviewItemsCountCallback = this.onAuthorPreviewItemsCountCallback.bind(this);
|
this.onAuthorPreviewItemsCountCallback = this.onAuthorPreviewItemsCountCallback.bind(this);
|
||||||
this.getCountFunc = this.getCountFunc.bind(this);
|
this.getCountFunc = this.getCountFunc.bind(this);
|
||||||
this.changeRequestQuery = this.changeRequestQuery.bind(this);
|
this.changeRequestQuery = this.changeRequestQuery.bind(this);
|
||||||
this.onToggleFiltersClick = this.onToggleFiltersClick.bind(this);
|
this.onToggleFiltersClick = this.onToggleFiltersClick.bind(this);
|
||||||
this.onToggleTagsClick = this.onToggleTagsClick.bind(this);
|
this.onToggleTagsClick = this.onToggleTagsClick.bind(this);
|
||||||
this.onToggleSortingClick = this.onToggleSortingClick.bind(this);
|
this.onToggleSortingClick = this.onToggleSortingClick.bind(this);
|
||||||
this.onFiltersUpdate = this.onFiltersUpdate.bind(this);
|
this.onFiltersUpdate = this.onFiltersUpdate.bind(this);
|
||||||
this.onTagSelect = this.onTagSelect.bind(this);
|
this.onTagSelect = this.onTagSelect.bind(this);
|
||||||
this.onSortSelect = this.onSortSelect.bind(this);
|
this.onSortSelect = this.onSortSelect.bind(this);
|
||||||
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
|
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
|
||||||
|
|
||||||
ProfilePageStore.on('load-author-data', this.authorDataLoad);
|
ProfilePageStore.on('load-author-data', this.authorDataLoad);
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
ProfilePageActions.load_author_data();
|
|
||||||
}
|
|
||||||
|
|
||||||
authorDataLoad() {
|
|
||||||
const author = ProfilePageStore.get('author-data');
|
|
||||||
|
|
||||||
let requestUrl = this.state.requestUrl;
|
|
||||||
|
|
||||||
if (author) {
|
|
||||||
if (this.state.query) {
|
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_with_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs;
|
|
||||||
} else {
|
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_with_me' + this.state.filterArgs;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
componentDidMount() {
|
||||||
author: author,
|
ProfilePageActions.load_author_data();
|
||||||
requestUrl: requestUrl,
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAuthorPreviewItemsCountCallback(totalAuthorPreviewItems) {
|
authorDataLoad() {
|
||||||
this.setState({
|
const author = ProfilePageStore.get('author-data');
|
||||||
uploadsPreviewItemsCount: totalAuthorPreviewItems,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getCountFunc(count) {
|
let requestUrl = this.state.requestUrl;
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
channelMediaCount: count,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
if (this.state.query) {
|
|
||||||
let title = '';
|
|
||||||
|
|
||||||
if (!count) {
|
if (author) {
|
||||||
title = 'No results for "' + this.state.query + '"';
|
if (this.state.query) {
|
||||||
} else if (1 === count) {
|
requestUrl =
|
||||||
title = '1 result for "' + this.state.query + '"';
|
ApiUrlContext._currentValue.media +
|
||||||
} else {
|
'?author=' +
|
||||||
title = count + ' results for "' + this.state.query + '"';
|
author.id +
|
||||||
}
|
'&show=shared_with_me&q=' +
|
||||||
|
encodeURIComponent(this.state.query) +
|
||||||
this.setState({
|
this.state.filterArgs;
|
||||||
title: title,
|
} else {
|
||||||
});
|
requestUrl =
|
||||||
|
ApiUrlContext._currentValue.media +
|
||||||
|
'?author=' +
|
||||||
|
author.id +
|
||||||
|
'&show=shared_with_me' +
|
||||||
|
this.state.filterArgs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeRequestQuery(newQuery) {
|
this.setState({
|
||||||
if (!this.state.author) {
|
author: author,
|
||||||
return;
|
requestUrl: requestUrl,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let requestUrl;
|
onAuthorPreviewItemsCountCallback(totalAuthorPreviewItems) {
|
||||||
|
this.setState({
|
||||||
if (newQuery) {
|
uploadsPreviewItemsCount: totalAuthorPreviewItems,
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me&q=' + encodeURIComponent(newQuery) + this.state.filterArgs;
|
});
|
||||||
} else {
|
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me' + this.state.filterArgs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = this.state.title;
|
getCountFunc(count) {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
channelMediaCount: count,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (this.state.query) {
|
||||||
|
let title = '';
|
||||||
|
|
||||||
if ('' === newQuery) {
|
if (!count) {
|
||||||
title = this.props.title;
|
title = 'No results for "' + this.state.query + '"';
|
||||||
|
} else if (1 === count) {
|
||||||
|
title = '1 result for "' + this.state.query + '"';
|
||||||
|
} else {
|
||||||
|
title = count + ' results for "' + this.state.query + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
title: title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
changeRequestQuery(newQuery) {
|
||||||
requestUrl: requestUrl,
|
|
||||||
query: newQuery,
|
|
||||||
title: title,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleFiltersClick() {
|
|
||||||
this.setState({
|
|
||||||
hiddenFilters: !this.state.hiddenFilters,
|
|
||||||
hiddenTags: true,
|
|
||||||
hiddenSorting: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleTagsClick() {
|
|
||||||
this.setState({
|
|
||||||
hiddenFilters: true,
|
|
||||||
hiddenTags: !this.state.hiddenTags,
|
|
||||||
hiddenSorting: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleSortingClick() {
|
|
||||||
this.setState({
|
|
||||||
hiddenFilters: true,
|
|
||||||
hiddenTags: true,
|
|
||||||
hiddenSorting: !this.state.hiddenSorting,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onTagSelect(tag) {
|
|
||||||
this.setState({ selectedTag: tag }, () => {
|
|
||||||
this.onFiltersUpdate({
|
|
||||||
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
|
|
||||||
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
|
|
||||||
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
|
|
||||||
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
|
|
||||||
sort_by: this.state.selectedSort,
|
|
||||||
tag: tag,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSortSelect(sortBy) {
|
|
||||||
this.setState({ selectedSort: sortBy }, () => {
|
|
||||||
this.onFiltersUpdate({
|
|
||||||
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
|
|
||||||
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
|
|
||||||
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
|
|
||||||
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
|
|
||||||
sort_by: sortBy,
|
|
||||||
tag: this.state.selectedTag,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onFiltersUpdate(updatedArgs) {
|
|
||||||
const args = {
|
|
||||||
media_type: null,
|
|
||||||
upload_date: null,
|
|
||||||
duration: null,
|
|
||||||
publish_state: null,
|
|
||||||
sort_by: null,
|
|
||||||
ordering: null,
|
|
||||||
t: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (updatedArgs.media_type) {
|
|
||||||
case 'video':
|
|
||||||
case 'audio':
|
|
||||||
case 'image':
|
|
||||||
case 'pdf':
|
|
||||||
args.media_type = updatedArgs.media_type;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (updatedArgs.upload_date) {
|
|
||||||
case 'today':
|
|
||||||
case 'this_week':
|
|
||||||
case 'this_month':
|
|
||||||
case 'this_year':
|
|
||||||
args.upload_date = updatedArgs.upload_date;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle duration filter
|
|
||||||
if (updatedArgs.duration && updatedArgs.duration !== 'all') {
|
|
||||||
args.duration = updatedArgs.duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle publish state filter
|
|
||||||
if (updatedArgs.publish_state && updatedArgs.publish_state !== 'all') {
|
|
||||||
args.publish_state = updatedArgs.publish_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (updatedArgs.sort_by) {
|
|
||||||
case 'date_added_desc':
|
|
||||||
// Default sorting, no need to add parameters
|
|
||||||
break;
|
|
||||||
case 'date_added_asc':
|
|
||||||
args.ordering = 'asc';
|
|
||||||
break;
|
|
||||||
case 'alphabetically_asc':
|
|
||||||
args.sort_by = 'title_asc';
|
|
||||||
break;
|
|
||||||
case 'alphabetically_desc':
|
|
||||||
args.sort_by = 'title_desc';
|
|
||||||
break;
|
|
||||||
case 'plays_least':
|
|
||||||
args.sort_by = 'views_asc';
|
|
||||||
break;
|
|
||||||
case 'plays_most':
|
|
||||||
args.sort_by = 'views_desc';
|
|
||||||
break;
|
|
||||||
case 'likes_least':
|
|
||||||
args.sort_by = 'likes_asc';
|
|
||||||
break;
|
|
||||||
case 'likes_most':
|
|
||||||
args.sort_by = 'likes_desc';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedArgs.tag && updatedArgs.tag !== 'all') {
|
|
||||||
args.t = updatedArgs.tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newArgs = [];
|
|
||||||
|
|
||||||
for (let arg in args) {
|
|
||||||
if (null !== args[arg]) {
|
|
||||||
newArgs.push(arg + '=' + args[arg]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
filterArgs: newArgs.length ? '&' + newArgs.join('&') : '',
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
if (!this.state.author) {
|
if (!this.state.author) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let requestUrl;
|
let requestUrl;
|
||||||
|
|
||||||
if (this.state.query) {
|
if (newQuery) {
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs;
|
requestUrl =
|
||||||
|
ApiUrlContext._currentValue.media +
|
||||||
|
'?author=' +
|
||||||
|
this.state.author.id +
|
||||||
|
'&show=shared_with_me&q=' +
|
||||||
|
encodeURIComponent(newQuery) +
|
||||||
|
this.state.filterArgs;
|
||||||
} else {
|
} else {
|
||||||
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me' + this.state.filterArgs;
|
requestUrl =
|
||||||
|
ApiUrlContext._currentValue.media +
|
||||||
|
'?author=' +
|
||||||
|
this.state.author.id +
|
||||||
|
'&show=shared_with_me' +
|
||||||
|
this.state.filterArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = this.state.title;
|
||||||
|
|
||||||
|
if ('' === newQuery) {
|
||||||
|
title = this.props.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
requestUrl: requestUrl,
|
requestUrl: requestUrl,
|
||||||
|
query: newQuery,
|
||||||
|
title: title,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onResponseDataLoaded(responseData) {
|
|
||||||
if (responseData && responseData.tags) {
|
|
||||||
const tags = responseData.tags.split(',').map((tag) => tag.trim()).filter((tag) => tag);
|
|
||||||
this.setState({ availableTags: tags });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pageContent() {
|
onToggleFiltersClick() {
|
||||||
const authorData = ProfilePageStore.get('author-data');
|
this.setState({
|
||||||
|
hiddenFilters: !this.state.hiddenFilters,
|
||||||
|
hiddenTags: true,
|
||||||
|
hiddenSorting: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
|
onToggleTagsClick() {
|
||||||
|
this.setState({
|
||||||
|
hiddenFilters: true,
|
||||||
|
hiddenTags: !this.state.hiddenTags,
|
||||||
|
hiddenSorting: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any filters are active
|
onToggleSortingClick() {
|
||||||
const hasActiveFilters = this.state.filterArgs && (
|
this.setState({
|
||||||
this.state.filterArgs.includes('media_type=') ||
|
hiddenFilters: true,
|
||||||
this.state.filterArgs.includes('upload_date=') ||
|
hiddenTags: true,
|
||||||
this.state.filterArgs.includes('duration=') ||
|
hiddenSorting: !this.state.hiddenSorting,
|
||||||
this.state.filterArgs.includes('publish_state=')
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
return [
|
onTagSelect(tag) {
|
||||||
this.state.author ? (
|
this.setState({ selectedTag: tag }, () => {
|
||||||
<ProfilePagesHeader
|
this.onFiltersUpdate({
|
||||||
key="ProfilePagesHeader"
|
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
|
||||||
author={this.state.author}
|
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
|
||||||
type="shared_with_me"
|
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
|
||||||
onQueryChange={this.changeRequestQuery}
|
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
|
||||||
onToggleFiltersClick={this.onToggleFiltersClick}
|
sort_by: this.state.selectedSort,
|
||||||
onToggleTagsClick={this.onToggleTagsClick}
|
tag: tag,
|
||||||
onToggleSortingClick={this.onToggleSortingClick}
|
});
|
||||||
hasActiveFilters={hasActiveFilters}
|
});
|
||||||
hasActiveTags={this.state.selectedTag !== 'all'}
|
}
|
||||||
hasActiveSort={this.state.selectedSort !== 'date_added_desc'}
|
|
||||||
/>
|
onSortSelect(sortBy) {
|
||||||
) : null,
|
this.setState({ selectedSort: sortBy }, () => {
|
||||||
this.state.author ? (
|
this.onFiltersUpdate({
|
||||||
<ProfilePagesContent key="ProfilePagesContent">
|
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
|
||||||
<MediaListWrapper
|
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
|
||||||
title={this.state.title}
|
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
|
||||||
className="items-list-ver"
|
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
|
||||||
>
|
sort_by: sortBy,
|
||||||
<ProfileMediaFilters hidden={this.state.hiddenFilters} tags={this.state.availableTags} onFiltersUpdate={this.onFiltersUpdate} />
|
tag: this.state.selectedTag,
|
||||||
<ProfileMediaTags hidden={this.state.hiddenTags} tags={this.state.availableTags} onTagSelect={this.onTagSelect} />
|
});
|
||||||
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
|
});
|
||||||
<LazyLoadItemListAsync
|
}
|
||||||
key={this.state.requestUrl}
|
|
||||||
requestUrl={this.state.requestUrl}
|
onFiltersUpdate(updatedArgs) {
|
||||||
hideAuthor={true}
|
const args = {
|
||||||
itemsCountCallback={this.state.requestUrl ? this.getCountFunc : null}
|
media_type: null,
|
||||||
hideViews={!PageStore.get('config-media-item').displayViews}
|
upload_date: null,
|
||||||
hideDate={!PageStore.get('config-media-item').displayPublishDate}
|
duration: null,
|
||||||
canEdit={false}
|
publish_state: null,
|
||||||
onResponseDataLoaded={this.onResponseDataLoaded}
|
sort_by: null,
|
||||||
/>
|
ordering: null,
|
||||||
{isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
|
t: null,
|
||||||
<EmptySharedWithMe name={this.state.author.name} />
|
};
|
||||||
) : null}
|
|
||||||
</MediaListWrapper>
|
switch (updatedArgs.media_type) {
|
||||||
</ProfilePagesContent>
|
case 'video':
|
||||||
) : null,
|
case 'audio':
|
||||||
];
|
case 'image':
|
||||||
}
|
case 'pdf':
|
||||||
|
args.media_type = updatedArgs.media_type;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (updatedArgs.upload_date) {
|
||||||
|
case 'today':
|
||||||
|
case 'this_week':
|
||||||
|
case 'this_month':
|
||||||
|
case 'this_year':
|
||||||
|
args.upload_date = updatedArgs.upload_date;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle duration filter
|
||||||
|
if (updatedArgs.duration && updatedArgs.duration !== 'all') {
|
||||||
|
args.duration = updatedArgs.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle publish state filter
|
||||||
|
if (updatedArgs.publish_state && updatedArgs.publish_state !== 'all') {
|
||||||
|
args.publish_state = updatedArgs.publish_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (updatedArgs.sort_by) {
|
||||||
|
case 'date_added_desc':
|
||||||
|
// Default sorting, no need to add parameters
|
||||||
|
break;
|
||||||
|
case 'date_added_asc':
|
||||||
|
args.ordering = 'asc';
|
||||||
|
break;
|
||||||
|
case 'alphabetically_asc':
|
||||||
|
args.sort_by = 'title_asc';
|
||||||
|
break;
|
||||||
|
case 'alphabetically_desc':
|
||||||
|
args.sort_by = 'title_desc';
|
||||||
|
break;
|
||||||
|
case 'plays_least':
|
||||||
|
args.sort_by = 'views_asc';
|
||||||
|
break;
|
||||||
|
case 'plays_most':
|
||||||
|
args.sort_by = 'views_desc';
|
||||||
|
break;
|
||||||
|
case 'likes_least':
|
||||||
|
args.sort_by = 'likes_asc';
|
||||||
|
break;
|
||||||
|
case 'likes_most':
|
||||||
|
args.sort_by = 'likes_desc';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedArgs.tag && updatedArgs.tag !== 'all') {
|
||||||
|
args.t = updatedArgs.tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newArgs = [];
|
||||||
|
|
||||||
|
for (let arg in args) {
|
||||||
|
if (null !== args[arg]) {
|
||||||
|
newArgs.push(arg + '=' + args[arg]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
filterArgs: newArgs.length ? '&' + newArgs.join('&') : '',
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
if (!this.state.author) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let requestUrl;
|
||||||
|
|
||||||
|
if (this.state.query) {
|
||||||
|
requestUrl =
|
||||||
|
ApiUrlContext._currentValue.media +
|
||||||
|
'?author=' +
|
||||||
|
this.state.author.id +
|
||||||
|
'&show=shared_with_me&q=' +
|
||||||
|
encodeURIComponent(this.state.query) +
|
||||||
|
this.state.filterArgs;
|
||||||
|
} else {
|
||||||
|
requestUrl =
|
||||||
|
ApiUrlContext._currentValue.media +
|
||||||
|
'?author=' +
|
||||||
|
this.state.author.id +
|
||||||
|
'&show=shared_with_me' +
|
||||||
|
this.state.filterArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
requestUrl: requestUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onResponseDataLoaded(responseData) {
|
||||||
|
if (responseData && responseData.tags) {
|
||||||
|
const tags = responseData.tags
|
||||||
|
.split(',')
|
||||||
|
.map((tag) => tag.trim())
|
||||||
|
.filter((tag) => tag);
|
||||||
|
this.setState({ availableTags: tags });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageContent() {
|
||||||
|
const authorData = ProfilePageStore.get('author-data');
|
||||||
|
|
||||||
|
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
|
||||||
|
|
||||||
|
// Check if any filters are active
|
||||||
|
const hasActiveFilters =
|
||||||
|
this.state.filterArgs &&
|
||||||
|
(this.state.filterArgs.includes('media_type=') ||
|
||||||
|
this.state.filterArgs.includes('upload_date=') ||
|
||||||
|
this.state.filterArgs.includes('duration=') ||
|
||||||
|
this.state.filterArgs.includes('publish_state='));
|
||||||
|
|
||||||
|
return [
|
||||||
|
this.state.author ? (
|
||||||
|
<ProfilePagesHeader
|
||||||
|
key="ProfilePagesHeader"
|
||||||
|
author={this.state.author}
|
||||||
|
type="shared_with_me"
|
||||||
|
onQueryChange={this.changeRequestQuery}
|
||||||
|
onToggleFiltersClick={this.onToggleFiltersClick}
|
||||||
|
onToggleTagsClick={this.onToggleTagsClick}
|
||||||
|
onToggleSortingClick={this.onToggleSortingClick}
|
||||||
|
hasActiveFilters={hasActiveFilters}
|
||||||
|
hasActiveTags={this.state.selectedTag !== 'all'}
|
||||||
|
hasActiveSort={this.state.selectedSort !== 'date_added_desc'}
|
||||||
|
hideChannelBanner={inEmbeddedApp()}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
this.state.author ? (
|
||||||
|
<ProfilePagesContent key="ProfilePagesContent">
|
||||||
|
<MediaListWrapper title={this.state.title} className="items-list-ver">
|
||||||
|
<ProfileMediaFilters
|
||||||
|
hidden={this.state.hiddenFilters}
|
||||||
|
tags={this.state.availableTags}
|
||||||
|
onFiltersUpdate={this.onFiltersUpdate}
|
||||||
|
/>
|
||||||
|
<ProfileMediaTags
|
||||||
|
hidden={this.state.hiddenTags}
|
||||||
|
tags={this.state.availableTags}
|
||||||
|
onTagSelect={this.onTagSelect}
|
||||||
|
/>
|
||||||
|
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
|
||||||
|
<LazyLoadItemListAsync
|
||||||
|
key={this.state.requestUrl}
|
||||||
|
requestUrl={this.state.requestUrl}
|
||||||
|
hideAuthor={true}
|
||||||
|
itemsCountCallback={this.state.requestUrl ? this.getCountFunc : null}
|
||||||
|
hideViews={!PageStore.get('config-media-item').displayViews}
|
||||||
|
hideDate={!PageStore.get('config-media-item').displayPublishDate}
|
||||||
|
canEdit={false}
|
||||||
|
onResponseDataLoaded={this.onResponseDataLoaded}
|
||||||
|
/>
|
||||||
|
{isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
|
||||||
|
<EmptySharedWithMe name={this.state.author.name} />
|
||||||
|
) : null}
|
||||||
|
</MediaListWrapper>
|
||||||
|
</ProfilePagesContent>
|
||||||
|
) : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfileSharedWithMePage.propTypes = {
|
ProfileSharedWithMePage.propTypes = {
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
ProfileSharedWithMePage.defaultProps = {
|
ProfileSharedWithMePage.defaultProps = {
|
||||||
title: 'Shared with me',
|
title: 'Shared with me',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PageStore, MediaPageStore } from '../utils/stores/';
|
import { PageStore, MediaPageStore } from '../utils/stores/';
|
||||||
import { MediaPageActions } from '../utils/actions/';
|
import { MediaPageActions } from '../utils/actions/';
|
||||||
|
import { inEmbeddedApp } from '../utils/helpers/';
|
||||||
import ViewerError from '../components/media-page/ViewerError';
|
import ViewerError from '../components/media-page/ViewerError';
|
||||||
import ViewerInfo from '../components/media-page/ViewerInfo';
|
import ViewerInfo from '../components/media-page/ViewerInfo';
|
||||||
import ViewerSidebar from '../components/media-page/ViewerSidebar';
|
import ViewerSidebar from '../components/media-page/ViewerSidebar';
|
||||||
@@ -10,102 +11,102 @@ import '../components/media-page/MediaPage.scss';
|
|||||||
const wideLayoutBreakpoint = 1216;
|
const wideLayoutBreakpoint = 1216;
|
||||||
|
|
||||||
export class _MediaPage extends Page {
|
export class _MediaPage extends Page {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props, 'media');
|
super(props, 'media');
|
||||||
|
|
||||||
const isWideLayout = wideLayoutBreakpoint <= window.innerWidth;
|
const isWideLayout = wideLayoutBreakpoint <= window.innerWidth;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
mediaLoaded: false,
|
mediaLoaded: false,
|
||||||
mediaLoadFailed: false,
|
mediaLoadFailed: false,
|
||||||
wideLayout: isWideLayout,
|
wideLayout: isWideLayout,
|
||||||
infoAndSidebarViewType: !isWideLayout ? 0 : 1,
|
infoAndSidebarViewType: !isWideLayout ? 0 : 1,
|
||||||
viewerClassname: 'cf viewer-section viewer-wide',
|
viewerClassname: 'cf viewer-section viewer-wide',
|
||||||
viewerNestedClassname: 'viewer-section-nested',
|
viewerNestedClassname: 'viewer-section-nested',
|
||||||
pagePlaylistLoaded: false,
|
pagePlaylistLoaded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onWindowResize = this.onWindowResize.bind(this);
|
this.onWindowResize = this.onWindowResize.bind(this);
|
||||||
this.onMediaLoad = this.onMediaLoad.bind(this);
|
this.onMediaLoad = this.onMediaLoad.bind(this);
|
||||||
this.onMediaLoadError = this.onMediaLoadError.bind(this);
|
this.onMediaLoadError = this.onMediaLoadError.bind(this);
|
||||||
this.onPagePlaylistLoad = this.onPagePlaylistLoad.bind(this);
|
this.onPagePlaylistLoad = this.onPagePlaylistLoad.bind(this);
|
||||||
|
|
||||||
MediaPageStore.on('loaded_media_data', this.onMediaLoad);
|
MediaPageStore.on('loaded_media_data', this.onMediaLoad);
|
||||||
MediaPageStore.on('loaded_media_error', this.onMediaLoadError);
|
MediaPageStore.on('loaded_media_error', this.onMediaLoadError);
|
||||||
MediaPageStore.on('loaded_page_playlist_data', this.onPagePlaylistLoad);
|
MediaPageStore.on('loaded_page_playlist_data', this.onPagePlaylistLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
MediaPageActions.loadMediaData();
|
MediaPageActions.loadMediaData();
|
||||||
// FIXME: Is not neccessary to check on every window dimension for changes...
|
// FIXME: Is not neccessary to check on every window dimension for changes...
|
||||||
PageStore.on('window_resize', this.onWindowResize);
|
PageStore.on('window_resize', this.onWindowResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
onPagePlaylistLoad() {
|
onPagePlaylistLoad() {
|
||||||
this.setState({
|
this.setState({
|
||||||
pagePlaylistLoaded: true,
|
pagePlaylistLoaded: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onWindowResize() {
|
onWindowResize() {
|
||||||
const isWideLayout = wideLayoutBreakpoint <= window.innerWidth;
|
const isWideLayout = wideLayoutBreakpoint <= window.innerWidth;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
wideLayout: isWideLayout,
|
wideLayout: isWideLayout,
|
||||||
infoAndSidebarViewType: !isWideLayout || (MediaPageStore.isVideo() && this.state.theaterMode) ? 0 : 1,
|
infoAndSidebarViewType: !isWideLayout || (MediaPageStore.isVideo() && this.state.theaterMode) ? 0 : 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMediaLoad() {
|
onMediaLoad() {
|
||||||
this.setState({ mediaLoaded: true });
|
this.setState({ mediaLoaded: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onMediaLoadError() {
|
onMediaLoadError() {
|
||||||
this.setState({ mediaLoadFailed: true });
|
this.setState({ mediaLoadFailed: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
viewerContainerContent() {
|
viewerContainerContent() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pageContent() {
|
pageContent() {
|
||||||
return this.state.mediaLoadFailed ? (
|
return this.state.mediaLoadFailed ? (
|
||||||
<div className={this.state.viewerClassname}>
|
<div className={this.state.viewerClassname}>
|
||||||
<ViewerError />
|
<ViewerError />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={this.state.viewerClassname}>
|
<div className={this.state.viewerClassname}>
|
||||||
<div className="viewer-container" key="viewer-container">
|
<div className="viewer-container" key="viewer-container">
|
||||||
{this.state.mediaLoaded ? this.viewerContainerContent() : null}
|
{this.state.mediaLoaded ? this.viewerContainerContent() : null}
|
||||||
</div>
|
</div>
|
||||||
<div key="viewer-section-nested" className={this.state.viewerNestedClassname}>
|
<div key="viewer-section-nested" className={this.state.viewerNestedClassname}>
|
||||||
{!this.state.infoAndSidebarViewType
|
{!this.state.infoAndSidebarViewType
|
||||||
? [
|
? [
|
||||||
<ViewerInfo key="viewer-info" />,
|
<ViewerInfo key="viewer-info" />,
|
||||||
this.state.pagePlaylistLoaded ? (
|
!inEmbeddedApp() && this.state.pagePlaylistLoaded ? (
|
||||||
<ViewerSidebar
|
<ViewerSidebar
|
||||||
key="viewer-sidebar"
|
key="viewer-sidebar"
|
||||||
mediaId={MediaPageStore.get('media-id')}
|
mediaId={MediaPageStore.get('media-id')}
|
||||||
playlistData={MediaPageStore.get('playlist-data')}
|
playlistData={MediaPageStore.get('playlist-data')}
|
||||||
/>
|
/>
|
||||||
) : null,
|
) : null,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
this.state.pagePlaylistLoaded ? (
|
!inEmbeddedApp() && this.state.pagePlaylistLoaded ? (
|
||||||
<ViewerSidebar
|
<ViewerSidebar
|
||||||
key="viewer-sidebar"
|
key="viewer-sidebar"
|
||||||
mediaId={MediaPageStore.get('media-id')}
|
mediaId={MediaPageStore.get('media-id')}
|
||||||
playlistData={MediaPageStore.get('playlist-data')}
|
playlistData={MediaPageStore.get('playlist-data')}
|
||||||
/>
|
/>
|
||||||
) : null,
|
) : null,
|
||||||
<ViewerInfo key="viewer-info" />,
|
<ViewerInfo key="viewer-info" />,
|
||||||
]}
|
]}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
// FIXME: 'VideoViewerStore' is used only in case of video media, but is included in every media page code.
|
// FIXME: 'VideoViewerStore' is used only in case of video media, but is included in every media page code.
|
||||||
import { PageStore, MediaPageStore, VideoViewerStore } from '../utils/stores/';
|
import { PageStore, MediaPageStore, VideoViewerStore } from '../utils/stores/';
|
||||||
import { MediaPageActions } from '../utils/actions/';
|
import { MediaPageActions } from '../utils/actions/';
|
||||||
|
import { inEmbeddedApp } from '../utils/helpers/';
|
||||||
import ViewerInfoVideo from '../components/media-page/ViewerInfoVideo';
|
import ViewerInfoVideo from '../components/media-page/ViewerInfoVideo';
|
||||||
import ViewerError from '../components/media-page/ViewerError';
|
import ViewerError from '../components/media-page/ViewerError';
|
||||||
import ViewerSidebar from '../components/media-page/ViewerSidebar';
|
import ViewerSidebar from '../components/media-page/ViewerSidebar';
|
||||||
@@ -11,118 +12,119 @@ import _MediaPage from './_MediaPage';
|
|||||||
const wideLayoutBreakpoint = 1216;
|
const wideLayoutBreakpoint = 1216;
|
||||||
|
|
||||||
export class _VideoMediaPage extends Page {
|
export class _VideoMediaPage extends Page {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props, 'media');
|
super(props, 'media');
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
wideLayout: wideLayoutBreakpoint <= window.innerWidth,
|
wideLayout: wideLayoutBreakpoint <= window.innerWidth,
|
||||||
mediaLoaded: false,
|
mediaLoaded: false,
|
||||||
mediaLoadFailed: false,
|
mediaLoadFailed: false,
|
||||||
isVideoMedia: false,
|
isVideoMedia: false,
|
||||||
theaterMode: false, // FIXME: Used only in case of video media, but is included in every media page code.
|
theaterMode: false, // FIXME: Used only in case of video media, but is included in every media page code.
|
||||||
pagePlaylistLoaded: false,
|
pagePlaylistLoaded: false,
|
||||||
pagePlaylistData: MediaPageStore.get('playlist-data'),
|
pagePlaylistData: MediaPageStore.get('playlist-data'),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onWindowResize = this.onWindowResize.bind(this);
|
this.onWindowResize = this.onWindowResize.bind(this);
|
||||||
this.onMediaLoad = this.onMediaLoad.bind(this);
|
this.onMediaLoad = this.onMediaLoad.bind(this);
|
||||||
this.onMediaLoadError = this.onMediaLoadError.bind(this);
|
this.onMediaLoadError = this.onMediaLoadError.bind(this);
|
||||||
this.onPagePlaylistLoad = this.onPagePlaylistLoad.bind(this);
|
this.onPagePlaylistLoad = this.onPagePlaylistLoad.bind(this);
|
||||||
|
|
||||||
MediaPageStore.on('loaded_media_data', this.onMediaLoad);
|
MediaPageStore.on('loaded_media_data', this.onMediaLoad);
|
||||||
MediaPageStore.on('loaded_media_error', this.onMediaLoadError);
|
MediaPageStore.on('loaded_media_error', this.onMediaLoadError);
|
||||||
MediaPageStore.on('loaded_page_playlist_data', this.onPagePlaylistLoad);
|
MediaPageStore.on('loaded_page_playlist_data', this.onPagePlaylistLoad);
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
MediaPageActions.loadMediaData();
|
|
||||||
// FIXME: Is not neccessary to check on every window dimension for changes...
|
|
||||||
PageStore.on('window_resize', this.onWindowResize);
|
|
||||||
}
|
|
||||||
|
|
||||||
onWindowResize() {
|
|
||||||
this.setState({
|
|
||||||
wideLayout: wideLayoutBreakpoint <= window.innerWidth,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPagePlaylistLoad() {
|
|
||||||
this.setState({
|
|
||||||
pagePlaylistLoaded: true,
|
|
||||||
pagePlaylistData: MediaPageStore.get('playlist-data'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMediaLoad() {
|
|
||||||
const isVideoMedia = 'video' === MediaPageStore.get('media-type') || 'audio' === MediaPageStore.get('media-type');
|
|
||||||
|
|
||||||
if (isVideoMedia) {
|
|
||||||
this.onViewerModeChange = this.onViewerModeChange.bind(this);
|
|
||||||
|
|
||||||
VideoViewerStore.on('changed_viewer_mode', this.onViewerModeChange);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
mediaLoaded: true,
|
|
||||||
isVideoMedia: isVideoMedia,
|
|
||||||
theaterMode: VideoViewerStore.get('in-theater-mode'),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
mediaLoaded: true,
|
|
||||||
isVideoMedia: isVideoMedia,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onViewerModeChange() {
|
componentDidMount() {
|
||||||
this.setState({ theaterMode: VideoViewerStore.get('in-theater-mode') });
|
MediaPageActions.loadMediaData();
|
||||||
}
|
// FIXME: Is not neccessary to check on every window dimension for changes...
|
||||||
|
PageStore.on('window_resize', this.onWindowResize);
|
||||||
|
}
|
||||||
|
|
||||||
onMediaLoadError(a) {
|
onWindowResize() {
|
||||||
this.setState({ mediaLoadFailed: true });
|
this.setState({
|
||||||
}
|
wideLayout: wideLayoutBreakpoint <= window.innerWidth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pageContent() {
|
onPagePlaylistLoad() {
|
||||||
const viewerClassname = 'cf viewer-section' + (this.state.theaterMode ? ' theater-mode' : ' viewer-wide');
|
this.setState({
|
||||||
const viewerNestedClassname = 'viewer-section-nested' + (this.state.theaterMode ? ' viewer-section' : '');
|
pagePlaylistLoaded: true,
|
||||||
|
pagePlaylistData: MediaPageStore.get('playlist-data'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return this.state.mediaLoadFailed ? (
|
onMediaLoad() {
|
||||||
<div className={viewerClassname}>
|
const isVideoMedia =
|
||||||
<ViewerError />
|
'video' === MediaPageStore.get('media-type') || 'audio' === MediaPageStore.get('media-type');
|
||||||
</div>
|
|
||||||
) : (
|
if (isVideoMedia) {
|
||||||
<div className={viewerClassname}>
|
this.onViewerModeChange = this.onViewerModeChange.bind(this);
|
||||||
{[
|
|
||||||
<div className="viewer-container" key="viewer-container">
|
VideoViewerStore.on('changed_viewer_mode', this.onViewerModeChange);
|
||||||
{this.state.mediaLoaded && this.state.pagePlaylistLoaded
|
|
||||||
? this.viewerContainerContent(MediaPageStore.get('media-data'))
|
this.setState({
|
||||||
: null}
|
mediaLoaded: true,
|
||||||
</div>,
|
isVideoMedia: isVideoMedia,
|
||||||
<div key="viewer-section-nested" className={viewerNestedClassname}>
|
theaterMode: VideoViewerStore.get('in-theater-mode'),
|
||||||
{!this.state.wideLayout || (this.state.isVideoMedia && this.state.theaterMode)
|
});
|
||||||
? [
|
} else {
|
||||||
<ViewerInfoVideo key="viewer-info" />,
|
this.setState({
|
||||||
this.state.pagePlaylistLoaded ? (
|
mediaLoaded: true,
|
||||||
<ViewerSidebar
|
isVideoMedia: isVideoMedia,
|
||||||
key="viewer-sidebar"
|
});
|
||||||
mediaId={MediaPageStore.get('media-id')}
|
}
|
||||||
playlistData={MediaPageStore.get('playlist-data')}
|
}
|
||||||
/>
|
|
||||||
) : null,
|
onViewerModeChange() {
|
||||||
]
|
this.setState({ theaterMode: VideoViewerStore.get('in-theater-mode') });
|
||||||
: [
|
}
|
||||||
this.state.pagePlaylistLoaded ? (
|
|
||||||
<ViewerSidebar
|
onMediaLoadError(a) {
|
||||||
key="viewer-sidebar"
|
this.setState({ mediaLoadFailed: true });
|
||||||
mediaId={MediaPageStore.get('media-id')}
|
}
|
||||||
playlistData={MediaPageStore.get('playlist-data')}
|
|
||||||
/>
|
pageContent() {
|
||||||
) : null,
|
const viewerClassname = 'cf viewer-section' + (this.state.theaterMode ? ' theater-mode' : ' viewer-wide');
|
||||||
<ViewerInfoVideo key="viewer-info" />,
|
const viewerNestedClassname = 'viewer-section-nested' + (this.state.theaterMode ? ' viewer-section' : '');
|
||||||
|
|
||||||
|
return this.state.mediaLoadFailed ? (
|
||||||
|
<div className={viewerClassname}>
|
||||||
|
<ViewerError />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={viewerClassname}>
|
||||||
|
{[
|
||||||
|
<div className="viewer-container" key="viewer-container">
|
||||||
|
{this.state.mediaLoaded && this.state.pagePlaylistLoaded
|
||||||
|
? this.viewerContainerContent(MediaPageStore.get('media-data'))
|
||||||
|
: null}
|
||||||
|
</div>,
|
||||||
|
<div key="viewer-section-nested" className={viewerNestedClassname}>
|
||||||
|
{!this.state.wideLayout || (this.state.isVideoMedia && this.state.theaterMode)
|
||||||
|
? [
|
||||||
|
<ViewerInfoVideo key="viewer-info" />,
|
||||||
|
!inEmbeddedApp() && this.state.pagePlaylistLoaded ? (
|
||||||
|
<ViewerSidebar
|
||||||
|
key="viewer-sidebar"
|
||||||
|
mediaId={MediaPageStore.get('media-id')}
|
||||||
|
playlistData={MediaPageStore.get('playlist-data')}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
!inEmbeddedApp() && this.state.pagePlaylistLoaded ? (
|
||||||
|
<ViewerSidebar
|
||||||
|
key="viewer-sidebar"
|
||||||
|
mediaId={MediaPageStore.get('media-id')}
|
||||||
|
playlistData={MediaPageStore.get('playlist-data')}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
<ViewerInfoVideo key="viewer-info" />,
|
||||||
|
]}
|
||||||
|
</div>,
|
||||||
]}
|
]}
|
||||||
</div>,
|
</div>
|
||||||
]}
|
);
|
||||||
</div>
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +1,103 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { BrowserCache } from '../classes/';
|
import { BrowserCache } from '../classes/';
|
||||||
import { PageStore } from '../stores/';
|
import { PageStore } from '../stores/';
|
||||||
import { addClassname, removeClassname } from '../helpers/';
|
import { addClassname, removeClassname, inEmbeddedApp } from '../helpers/';
|
||||||
import SiteContext from './SiteContext';
|
import SiteContext from './SiteContext';
|
||||||
|
|
||||||
let slidingSidebarTimeout;
|
let slidingSidebarTimeout;
|
||||||
|
|
||||||
function onSidebarVisibilityChange(visibleSidebar) {
|
function onSidebarVisibilityChange(visibleSidebar) {
|
||||||
clearTimeout(slidingSidebarTimeout);
|
clearTimeout(slidingSidebarTimeout);
|
||||||
|
|
||||||
addClassname(document.body, 'sliding-sidebar');
|
addClassname(document.body, 'sliding-sidebar');
|
||||||
|
|
||||||
slidingSidebarTimeout = setTimeout(function () {
|
|
||||||
if ('media' === PageStore.get('current-page')) {
|
|
||||||
if (visibleSidebar) {
|
|
||||||
addClassname(document.body, 'overflow-hidden');
|
|
||||||
} else {
|
|
||||||
removeClassname(document.body, 'overflow-hidden');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!visibleSidebar || 767 < window.innerWidth) {
|
|
||||||
removeClassname(document.body, 'overflow-hidden');
|
|
||||||
} else {
|
|
||||||
addClassname(document.body, 'overflow-hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visibleSidebar) {
|
|
||||||
addClassname(document.body, 'visible-sidebar');
|
|
||||||
} else {
|
|
||||||
removeClassname(document.body, 'visible-sidebar');
|
|
||||||
}
|
|
||||||
|
|
||||||
slidingSidebarTimeout = setTimeout(function () {
|
slidingSidebarTimeout = setTimeout(function () {
|
||||||
slidingSidebarTimeout = null;
|
if ('media' === PageStore.get('current-page')) {
|
||||||
removeClassname(document.body, 'sliding-sidebar');
|
if (visibleSidebar) {
|
||||||
}, 220);
|
addClassname(document.body, 'overflow-hidden');
|
||||||
}, 20);
|
} else {
|
||||||
|
removeClassname(document.body, 'overflow-hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!visibleSidebar || 767 < window.innerWidth) {
|
||||||
|
removeClassname(document.body, 'overflow-hidden');
|
||||||
|
} else {
|
||||||
|
addClassname(document.body, 'overflow-hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibleSidebar) {
|
||||||
|
addClassname(document.body, 'visible-sidebar');
|
||||||
|
} else {
|
||||||
|
removeClassname(document.body, 'visible-sidebar');
|
||||||
|
}
|
||||||
|
|
||||||
|
slidingSidebarTimeout = setTimeout(function () {
|
||||||
|
slidingSidebarTimeout = null;
|
||||||
|
removeClassname(document.body, 'sliding-sidebar');
|
||||||
|
}, 220);
|
||||||
|
}, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LayoutContext = createContext();
|
export const LayoutContext = createContext();
|
||||||
|
|
||||||
export const LayoutProvider = ({ children }) => {
|
export const LayoutProvider = ({ children }) => {
|
||||||
const site = useContext(SiteContext);
|
const site = useContext(SiteContext);
|
||||||
const cache = new BrowserCache('MediaCMS[' + site.id + '][layout]', 86400);
|
const cache = new BrowserCache('MediaCMS[' + site.id + '][layout]', 86400);
|
||||||
|
|
||||||
const enabledSidebar = !!(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar'));
|
const isMediaPage = useMemo(() => PageStore.get('current-page') === 'media', []);
|
||||||
|
const isEmbeddedApp = useMemo(() => inEmbeddedApp(), []);
|
||||||
|
|
||||||
const [visibleSidebar, setVisibleSidebar] = useState(cache.get('visible-sidebar'));
|
const enabledSidebar = Boolean(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar'));
|
||||||
const [visibleMobileSearch, setVisibleMobileSearch] = useState(false);
|
|
||||||
|
|
||||||
const toggleMobileSearch = () => {
|
const [visibleSidebar, setVisibleSidebar] = useState(cache.get('visible-sidebar'));
|
||||||
setVisibleMobileSearch(!visibleMobileSearch);
|
const [visibleMobileSearch, setVisibleMobileSearch] = useState(false);
|
||||||
};
|
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleMobileSearch = () => {
|
||||||
const newval = !visibleSidebar;
|
setVisibleMobileSearch(!visibleMobileSearch);
|
||||||
onSidebarVisibilityChange(newval);
|
};
|
||||||
setVisibleSidebar(newval);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
const toggleSidebar = () => {
|
||||||
if (visibleSidebar) {
|
const newval = !visibleSidebar;
|
||||||
addClassname(document.body, 'visible-sidebar');
|
onSidebarVisibilityChange(newval);
|
||||||
} else {
|
setVisibleSidebar(newval);
|
||||||
removeClassname(document.body, 'visible-sidebar');
|
};
|
||||||
}
|
|
||||||
if ('media' !== PageStore.get('current-page') && 1023 < window.innerWidth) {
|
|
||||||
cache.set('visible-sidebar', visibleSidebar);
|
|
||||||
}
|
|
||||||
}, [visibleSidebar]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
PageStore.once('page_init', () => {
|
if (!isEmbeddedApp && visibleSidebar) {
|
||||||
if ('media' === PageStore.get('current-page')) {
|
addClassname(document.body, 'visible-sidebar');
|
||||||
setVisibleSidebar(false);
|
} else {
|
||||||
removeClassname(document.body, 'visible-sidebar');
|
removeClassname(document.body, 'visible-sidebar');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
setVisibleSidebar(
|
if (!isEmbeddedApp && !isMediaPage && 1023 < window.innerWidth) {
|
||||||
'media' !== PageStore.get('current-page') &&
|
cache.set('visible-sidebar', visibleSidebar);
|
||||||
1023 < window.innerWidth &&
|
}
|
||||||
(null === visibleSidebar || visibleSidebar)
|
}, [isEmbeddedApp, isMediaPage, visibleSidebar]);
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const value = {
|
useEffect(() => {
|
||||||
enabledSidebar,
|
PageStore.once('page_init', () => {
|
||||||
visibleSidebar,
|
if (isEmbeddedApp || isMediaPage) {
|
||||||
setVisibleSidebar,
|
setVisibleSidebar(false);
|
||||||
visibleMobileSearch,
|
removeClassname(document.body, 'visible-sidebar');
|
||||||
toggleMobileSearch,
|
}
|
||||||
toggleSidebar,
|
});
|
||||||
};
|
|
||||||
|
|
||||||
return <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>;
|
setVisibleSidebar(
|
||||||
|
!isEmbeddedApp && !isMediaPage && 1023 < window.innerWidth && (null === visibleSidebar || visibleSidebar)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
enabledSidebar,
|
||||||
|
visibleSidebar,
|
||||||
|
setVisibleSidebar,
|
||||||
|
visibleMobileSearch,
|
||||||
|
toggleMobileSearch,
|
||||||
|
toggleSidebar,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LayoutConsumer = LayoutContext.Consumer;
|
export const LayoutConsumer = LayoutContext.Consumer;
|
||||||
|
|||||||
20
frontend/src/static/js/utils/helpers/embeddedApp.ts
Normal file
20
frontend/src/static/js/utils/helpers/embeddedApp.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export function inEmbeddedApp() {
|
||||||
|
try {
|
||||||
|
const params = new URL(globalThis.location.href).searchParams;
|
||||||
|
const mode = params.get('mode');
|
||||||
|
|
||||||
|
if (mode === 'embed_mode') {
|
||||||
|
sessionStorage.setItem('media_cms_embed_mode', 'true');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'standard') {
|
||||||
|
sessionStorage.removeItem('media_cms_embed_mode');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionStorage.getItem('media_cms_embed_mode') === 'true';
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,3 +14,4 @@ export * from './quickSort';
|
|||||||
export * from './requests';
|
export * from './requests';
|
||||||
export { translateString } from './translate';
|
export { translateString } from './translate';
|
||||||
export { replaceString } from './replacementStrings';
|
export { replaceString } from './replacementStrings';
|
||||||
|
export * from './embeddedApp';
|
||||||
|
|||||||
@@ -3,64 +3,83 @@ import ReactDOM from 'react-dom';
|
|||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
import { LayoutProvider } from './contexts/LayoutContext';
|
import { LayoutProvider } from './contexts/LayoutContext';
|
||||||
import { UserProvider } from './contexts/UserContext';
|
import { UserProvider } from './contexts/UserContext';
|
||||||
|
import { inEmbeddedApp } from './helpers';
|
||||||
|
|
||||||
const AppProviders = ({ children }) => (
|
const AppProviders = ({ children }) => (
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<UserProvider>{children}</UserProvider>
|
<UserProvider>{children}</UserProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</LayoutProvider>
|
</LayoutProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
import { PageHeader, PageSidebar } from '../components/page-layout';
|
import { PageHeader, PageSidebar } from '../components/page-layout';
|
||||||
|
|
||||||
export function renderPage(idSelector, PageComponent) {
|
export function renderPage(idSelector, PageComponent) {
|
||||||
const appHeader = document.getElementById('app-header');
|
if (inEmbeddedApp()) {
|
||||||
const appSidebar = document.getElementById('app-sidebar');
|
globalThis.document.body.classList.add('embedded-app');
|
||||||
const appContent = idSelector ? document.getElementById(idSelector) : undefined;
|
globalThis.document.body.classList.remove('visible-sidebar');
|
||||||
|
|
||||||
if (appContent && PageComponent) {
|
const appContent = idSelector ? document.getElementById(idSelector) : undefined;
|
||||||
ReactDOM.render(
|
|
||||||
<AppProviders>
|
if (appContent && PageComponent) {
|
||||||
{appHeader ? ReactDOM.createPortal(<PageHeader />, appHeader) : null}
|
ReactDOM.render(
|
||||||
{appSidebar ? ReactDOM.createPortal(<PageSidebar />, appSidebar) : null}
|
<AppProviders>
|
||||||
<PageComponent />
|
<PageComponent />
|
||||||
</AppProviders>,
|
</AppProviders>,
|
||||||
appContent
|
appContent
|
||||||
);
|
);
|
||||||
} else if (appHeader && appSidebar) {
|
}
|
||||||
ReactDOM.render(
|
|
||||||
<AppProviders>
|
return;
|
||||||
{ReactDOM.createPortal(<PageHeader />, appHeader)}
|
}
|
||||||
<PageSidebar />
|
|
||||||
</AppProviders>,
|
const appContent = idSelector ? document.getElementById(idSelector) : undefined;
|
||||||
appSidebar
|
const appHeader = document.getElementById('app-header');
|
||||||
);
|
const appSidebar = document.getElementById('app-sidebar');
|
||||||
} else if (appHeader) {
|
|
||||||
ReactDOM.render(
|
if (appContent && PageComponent) {
|
||||||
<LayoutProvider>
|
ReactDOM.render(
|
||||||
<ThemeProvider>
|
<AppProviders>
|
||||||
<UserProvider>
|
{appHeader ? ReactDOM.createPortal(<PageHeader />, appHeader) : null}
|
||||||
<PageHeader />
|
{appSidebar ? ReactDOM.createPortal(<PageSidebar />, appSidebar) : null}
|
||||||
</UserProvider>
|
<PageComponent />
|
||||||
</ThemeProvider>
|
</AppProviders>,
|
||||||
</LayoutProvider>,
|
appContent
|
||||||
appSidebar
|
);
|
||||||
);
|
} else if (appHeader && appSidebar) {
|
||||||
} else if (appSidebar) {
|
ReactDOM.render(
|
||||||
ReactDOM.render(
|
<AppProviders>
|
||||||
<AppProviders>
|
{ReactDOM.createPortal(<PageHeader />, appHeader)}
|
||||||
<PageSidebar />
|
<PageSidebar />
|
||||||
</AppProviders>,
|
</AppProviders>,
|
||||||
appSidebar
|
appSidebar
|
||||||
);
|
);
|
||||||
}
|
} else if (appHeader) {
|
||||||
|
ReactDOM.render(
|
||||||
|
<LayoutProvider>
|
||||||
|
<ThemeProvider>
|
||||||
|
<UserProvider>
|
||||||
|
<PageHeader />
|
||||||
|
</UserProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</LayoutProvider>,
|
||||||
|
appSidebar
|
||||||
|
);
|
||||||
|
} else if (appSidebar) {
|
||||||
|
ReactDOM.render(
|
||||||
|
<AppProviders>
|
||||||
|
<PageSidebar />
|
||||||
|
</AppProviders>,
|
||||||
|
appSidebar
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderEmbedPage(idSelector, PageComponent) {
|
export function renderEmbedPage(idSelector, PageComponent) {
|
||||||
const appContent = idSelector ? document.getElementById(idSelector) : undefined;
|
const appContent = idSelector ? document.getElementById(idSelector) : undefined;
|
||||||
|
|
||||||
if (appContent && PageComponent) {
|
if (appContent && PageComponent) {
|
||||||
ReactDOM.render(<PageComponent />, appContent);
|
ReactDOM.render(<PageComponent />, appContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6902
package-lock.json
generated
Normal file
6902
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
package.json
Normal file
13
package.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "mediacms",
|
||||||
|
"version": "7.5.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"@semantic-release/github": "^11.0.3",
|
||||||
|
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||||
|
"conventional-changelog-conventionalcommits": "^9.0.0",
|
||||||
|
"semantic-release": "^24.2.6",
|
||||||
|
"semantic-release-replace-plugin": "^1.2.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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