mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-03-22 04:33:09 -04:00
Compare commits
19 Commits
v7.3.0
...
frontend-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20682a543a | ||
|
|
c8b47a7922 | ||
|
|
499196b0f6 | ||
|
|
374ae4de6e | ||
|
|
7a5fca6fd8 | ||
|
|
e9af15582f | ||
|
|
1b8e8aae6a | ||
|
|
df4b0422d5 | ||
|
|
0434f24691 | ||
|
|
c2043fafa1 | ||
|
|
9f9dd699b2 | ||
|
|
e2bc9399b9 | ||
|
|
45d94069b9 | ||
|
|
b7427869b6 | ||
|
|
11449c2187 | ||
|
|
f7c675596f | ||
|
|
36d815c0cf | ||
|
|
8f28b00a63 | ||
|
|
74952f68d7 |
@@ -1,3 +1,4 @@
|
||||
/templates/cms/*
|
||||
/templates/*.html
|
||||
*.scss
|
||||
*.scss
|
||||
/frontend/
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## [7.6.0](https://github.com/mediacms-io/mediacms/compare/v7.5.0...v7.6.0) (2026-02-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Create SECURITY.md ([#1485](https://github.com/mediacms-io/mediacms/issues/1485)) ([11449c2](https://github.com/mediacms-io/mediacms/commit/11449c2187d0f450b86915d88f92595a1825e4cf))
|
||||
|
||||
## [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
|
||||
|
||||
54
SECURITY.md
Normal file
54
SECURITY.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Security Policy
|
||||
|
||||
Thank you for helping improve the security of MediaCMS.
|
||||
We take security vulnerabilities seriously and appreciate responsible disclosure.
|
||||
|
||||
---
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in MediaCMS, **please do not open a public GitHub issue**.
|
||||
|
||||
Instead, report it using one of the following methods:
|
||||
|
||||
- **GitHub Security Advisories (preferred)**
|
||||
Use the "Report a vulnerability" feature in this repository.
|
||||
|
||||
- **Contact Form**
|
||||
Submit details via the official contact page:
|
||||
https://mediacms.io/contact/
|
||||
|
||||
Please include as much of the following information as possible:
|
||||
- Affected version(s)
|
||||
- Detailed description of the issue
|
||||
- Steps to reproduce (PoC if available)
|
||||
- Impact assessment (e.g. RCE, XSS, privilege escalation)
|
||||
- Any potential mitigations you are aware of
|
||||
|
||||
---
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security updates are provided for the **latest stable release** of MediaCMS.
|
||||
Older versions may not receive security patches.
|
||||
|
||||
---
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
- We aim to acknowledge reports within **7 days**
|
||||
- We aim to provide a fix or mitigation within **90 days**, depending on severity
|
||||
- Please allow us time to investigate before any public disclosure
|
||||
|
||||
We follow responsible disclosure practices and will coordinate disclosure timelines when appropriate.
|
||||
|
||||
---
|
||||
|
||||
## Recognition
|
||||
|
||||
At this time, MediaCMS does not operate a formal bug bounty program.
|
||||
However, we are happy to acknowledge valid security reports in release notes or advisories (with your permission).
|
||||
|
||||
---
|
||||
|
||||
Thank you for helping keep MediaCMS secure.
|
||||
@@ -1 +1 @@
|
||||
VERSION = "7.7"
|
||||
VERSION = "7.9"
|
||||
|
||||
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>
|
||||
`;
|
||||
textEl.textContent = 'Pause';
|
||||
} else if (direction === 'copy-url') {
|
||||
iconEl.innerHTML = `
|
||||
<div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;">
|
||||
<div style="
|
||||
width: ${circleSize};
|
||||
height: ${circleSize};
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
">
|
||||
<svg viewBox="0 0 24 24" width="${iconSize}" height="${iconSize}" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
textEl.textContent = '';
|
||||
} else if (direction === 'copy-embed') {
|
||||
iconEl.innerHTML = `
|
||||
<div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;">
|
||||
<div style="
|
||||
width: ${circleSize};
|
||||
height: ${circleSize};
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
">
|
||||
<svg viewBox="0 0 24 24" width="${iconSize}" height="${iconSize}" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));">
|
||||
<path d="M16 18l6-6-6-6"/>
|
||||
<path d="M8 6l-6 6 6 6"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
textEl.textContent = '';
|
||||
}
|
||||
|
||||
// Clear any text content in the text element
|
||||
@@ -239,6 +287,11 @@ class SeekIndicator extends Component {
|
||||
this.showTimeout = setTimeout(() => {
|
||||
this.hide();
|
||||
}, 500);
|
||||
} else if (direction === 'copy-url' || direction === 'copy-embed') {
|
||||
// Copy operations: 500ms (same as play/pause)
|
||||
this.showTimeout = setTimeout(() => {
|
||||
this.hide();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,22 @@ class EmbedInfoOverlay extends Component {
|
||||
this.authorThumbnail = options.authorThumbnail || '';
|
||||
this.videoTitle = options.videoTitle || 'Video';
|
||||
this.videoUrl = options.videoUrl || '';
|
||||
this.showTitle = options.showTitle !== undefined ? options.showTitle : true;
|
||||
this.showRelated = options.showRelated !== undefined ? options.showRelated : true;
|
||||
this.showUserAvatar = options.showUserAvatar !== undefined ? options.showUserAvatar : true;
|
||||
this.linkTitle = options.linkTitle !== undefined ? options.linkTitle : true;
|
||||
|
||||
// Initialize after player is ready
|
||||
this.player().ready(() => {
|
||||
this.createOverlay();
|
||||
if (this.showTitle) {
|
||||
this.createOverlay();
|
||||
} else {
|
||||
// Hide overlay element if showTitle is false
|
||||
const overlay = this.el();
|
||||
overlay.style.display = 'none';
|
||||
overlay.style.opacity = '0';
|
||||
overlay.style.visibility = 'hidden';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,7 +61,7 @@ class EmbedInfoOverlay extends Component {
|
||||
`;
|
||||
|
||||
// Create avatar container
|
||||
if (this.authorThumbnail) {
|
||||
if (this.authorThumbnail && this.showUserAvatar) {
|
||||
const avatarContainer = document.createElement('div');
|
||||
avatarContainer.className = 'embed-avatar-container';
|
||||
avatarContainer.style.cssText = `
|
||||
@@ -125,7 +137,7 @@ class EmbedInfoOverlay extends Component {
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
if (this.videoUrl) {
|
||||
if (this.videoUrl && this.linkTitle) {
|
||||
const titleLink = document.createElement('a');
|
||||
titleLink.href = this.videoUrl;
|
||||
titleLink.target = '_blank';
|
||||
@@ -186,10 +198,16 @@ class EmbedInfoOverlay extends Component {
|
||||
const player = this.player();
|
||||
const overlay = this.el();
|
||||
|
||||
// If showTitle is false, ensure overlay is hidden
|
||||
if (!this.showTitle) {
|
||||
overlay.style.display = 'none';
|
||||
overlay.style.opacity = '0';
|
||||
overlay.style.visibility = 'hidden';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync overlay visibility with control bar visibility
|
||||
const updateOverlayVisibility = () => {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
|
||||
if (!player.hasStarted()) {
|
||||
// Show overlay when video hasn't started (poster is showing) - like before
|
||||
overlay.style.opacity = '1';
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
.video-context-menu {
|
||||
position: fixed;
|
||||
background-color: #282828;
|
||||
border-radius: 4px;
|
||||
padding: 4px 0;
|
||||
min-width: 240px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
}
|
||||
|
||||
.video-context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.video-context-menu-item:hover {
|
||||
background-color: #3d3d3d;
|
||||
}
|
||||
|
||||
.video-context-menu-item:active {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.video-context-menu-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.video-context-menu-item span {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import './VideoContextMenu.css';
|
||||
|
||||
function VideoContextMenu({ visible, position, onClose, onCopyVideoUrl, onCopyVideoUrlAtTime, onCopyEmbedCode }) {
|
||||
const menuRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && menuRef.current) {
|
||||
// Position the menu
|
||||
menuRef.current.style.left = `${position.x}px`;
|
||||
menuRef.current.style.top = `${position.y}px`;
|
||||
|
||||
// Adjust if menu goes off screen
|
||||
const rect = menuRef.current.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if (rect.right > windowWidth) {
|
||||
menuRef.current.style.left = `${position.x - rect.width}px`;
|
||||
}
|
||||
if (rect.bottom > windowHeight) {
|
||||
menuRef.current.style.top = `${position.y - rect.height}px`;
|
||||
}
|
||||
}
|
||||
}, [visible, position]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e) => {
|
||||
if (visible && menuRef.current && !menuRef.current.contains(e.target)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (e) => {
|
||||
if (e.key === 'Escape' && visible) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (visible) {
|
||||
// Use capture phase to catch events earlier, before they can be stopped
|
||||
// Listen to both mousedown and click to ensure we catch all clicks
|
||||
document.addEventListener('mousedown', handleClickOutside, true);
|
||||
document.addEventListener('click', handleClickOutside, true);
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside, true);
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [visible, onClose]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div ref={menuRef} className="video-context-menu" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="video-context-menu-item" onClick={onCopyVideoUrl}>
|
||||
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span>Copy video URL</span>
|
||||
</div>
|
||||
<div className="video-context-menu-item" onClick={onCopyVideoUrlAtTime}>
|
||||
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span>Copy video URL at current time</span>
|
||||
</div>
|
||||
<div className="video-context-menu-item" onClick={onCopyEmbedCode}>
|
||||
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 18l6-6-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M8 6l-6 6 6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span>Copy embed code</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VideoContextMenu;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useMemo } from 'react';
|
||||
import React, { useEffect, useRef, useMemo, useState, useCallback } from 'react';
|
||||
import videojs from 'video.js';
|
||||
import 'video.js/dist/video-js.css';
|
||||
import '../../styles/embed.css';
|
||||
@@ -17,6 +17,7 @@ import CustomRemainingTime from '../controls/CustomRemainingTime';
|
||||
import CustomChaptersOverlay from '../controls/CustomChaptersOverlay';
|
||||
import CustomSettingsMenu from '../controls/CustomSettingsMenu';
|
||||
import SeekIndicator from '../controls/SeekIndicator';
|
||||
import VideoContextMenu from '../overlays/VideoContextMenu';
|
||||
import UserPreferences from '../../utils/UserPreferences';
|
||||
import PlayerConfig from '../../config/playerConfig';
|
||||
import { AutoplayHandler } from '../../utils/AutoplayHandler';
|
||||
@@ -169,7 +170,7 @@ const enableStandardButtonTooltips = (player) => {
|
||||
}, 500); // Delay to ensure all components are ready
|
||||
};
|
||||
|
||||
function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
function VideoJSPlayer({ videoId = 'default-video', showTitle = true, showRelated = true, showUserAvatar = true, linkTitle = true, urlTimestamp = null }) {
|
||||
const videoRef = useRef(null);
|
||||
const playerRef = useRef(null); // Track the player instance
|
||||
const userPreferences = useRef(new UserPreferences()); // User preferences instance
|
||||
@@ -177,25 +178,17 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
const keyboardHandler = useRef(null); // Keyboard handler instance
|
||||
const playbackEventHandler = useRef(null); // Playback event handler instance
|
||||
|
||||
// Context menu state
|
||||
const [contextMenuVisible, setContextMenuVisible] = useState(false);
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
// Check if this is an embed player (disable next video and autoplay features)
|
||||
const isEmbedPlayer = videoId === 'video-embed';
|
||||
|
||||
// Utility function to detect touch devices
|
||||
const isTouchDevice = useMemo(() => {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||
}, []);
|
||||
|
||||
// Utility function to detect iOS devices
|
||||
const isIOS = useMemo(() => {
|
||||
return (
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Environment-based development mode configuration
|
||||
const isDevMode = import.meta.env.VITE_DEV_MODE === 'true' || window.location.hostname.includes('vercel.app');
|
||||
// Safely access window.MEDIA_DATA with fallback using useMemo
|
||||
|
||||
// Read options from window.MEDIA_DATA if available (for consistency with embed logic)
|
||||
const mediaData = useMemo(
|
||||
() =>
|
||||
typeof window !== 'undefined' && window.MEDIA_DATA
|
||||
@@ -214,12 +207,37 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
},
|
||||
siteUrl: 'https://deic.mediacms.io',
|
||||
nextLink: 'https://deic.mediacms.io/view?m=elygiagorgechania',
|
||||
urlAutoplay: true,
|
||||
urlMuted: false,
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Helper to get effective value (prop or MEDIA_DATA or default)
|
||||
const getOption = (propKey, mediaDataKey, defaultValue) => {
|
||||
if (isEmbedPlayer) {
|
||||
if (mediaData[mediaDataKey] !== undefined) return mediaData[mediaDataKey];
|
||||
}
|
||||
return propKey !== undefined ? propKey : defaultValue;
|
||||
};
|
||||
|
||||
const finalShowTitle = getOption(showTitle, 'showTitle', true);
|
||||
const finalShowRelated = getOption(showRelated, 'showRelated', true);
|
||||
const finalShowUserAvatar = getOption(showUserAvatar, 'showUserAvatar', true);
|
||||
const finalLinkTitle = getOption(linkTitle, 'linkTitle', true);
|
||||
const finalTimestamp = getOption(urlTimestamp, 'urlTimestamp', null);
|
||||
|
||||
// Utility function to detect touch devices
|
||||
const isTouchDevice = useMemo(() => {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||
}, []);
|
||||
|
||||
// Utility function to detect iOS devices
|
||||
const isIOS = useMemo(() => {
|
||||
return (
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Define chapters as JSON object
|
||||
// Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON
|
||||
// CONDITIONAL LOGIC:
|
||||
@@ -531,8 +549,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
isPlayList: mediaData?.isPlayList,
|
||||
related_media: mediaData.data?.related_media || [],
|
||||
nextLink: mediaData?.nextLink || null,
|
||||
urlAutoplay: mediaData?.urlAutoplay || true,
|
||||
urlMuted: mediaData?.urlMuted || false,
|
||||
sources: getVideoSources(),
|
||||
};
|
||||
|
||||
@@ -738,6 +754,212 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Context menu handlers
|
||||
const handleContextMenu = useCallback((e) => {
|
||||
// Only handle if clicking on video player area
|
||||
const target = e.target;
|
||||
const isVideoPlayerArea =
|
||||
target.closest('.video-js') ||
|
||||
target.classList.contains('vjs-tech') ||
|
||||
target.tagName === 'VIDEO' ||
|
||||
target.closest('video');
|
||||
|
||||
if (isVideoPlayerArea) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY });
|
||||
setContextMenuVisible(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const closeContextMenu = () => {
|
||||
setContextMenuVisible(false);
|
||||
};
|
||||
|
||||
// Helper function to get media ID
|
||||
const getMediaId = () => {
|
||||
if (typeof window !== 'undefined' && window.MEDIA_DATA?.data?.friendly_token) {
|
||||
return window.MEDIA_DATA.data.friendly_token;
|
||||
}
|
||||
if (mediaData?.data?.friendly_token) {
|
||||
return mediaData.data.friendly_token;
|
||||
}
|
||||
// Try to get from URL (works for both main page and embed page)
|
||||
if (typeof window !== 'undefined') {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const mediaIdFromUrl = urlParams.get('m');
|
||||
if (mediaIdFromUrl) {
|
||||
return mediaIdFromUrl;
|
||||
}
|
||||
// Also check if we're on an embed page with media ID in path
|
||||
const pathMatch = window.location.pathname.match(/\/embed\/([^/?]+)/);
|
||||
if (pathMatch) {
|
||||
return pathMatch[1];
|
||||
}
|
||||
}
|
||||
return currentVideo.id || 'default-video';
|
||||
};
|
||||
|
||||
// Helper function to get base origin URL (handles embed mode)
|
||||
const getBaseOrigin = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// In embed mode, try to get origin from parent window if possible
|
||||
// Otherwise use current window origin
|
||||
try {
|
||||
// Check if we're in an iframe and can access parent
|
||||
if (window.parent !== window && window.parent.location.origin) {
|
||||
return window.parent.location.origin;
|
||||
}
|
||||
} catch {
|
||||
// Cross-origin iframe, use current origin
|
||||
}
|
||||
return window.location.origin;
|
||||
}
|
||||
return mediaData.siteUrl || 'https://deic.mediacms.io';
|
||||
};
|
||||
|
||||
// Helper function to get embed URL
|
||||
const getEmbedUrl = () => {
|
||||
const mediaId = getMediaId();
|
||||
const origin = getBaseOrigin();
|
||||
|
||||
// Try to get embed URL from config or construct it
|
||||
if (typeof window !== 'undefined' && window.MediaCMS?.config?.url?.embed) {
|
||||
return window.MediaCMS.config.url.embed + mediaId;
|
||||
}
|
||||
|
||||
// Fallback: construct embed URL (check if current URL is embed format)
|
||||
if (typeof window !== 'undefined' && window.location.pathname.includes('/embed')) {
|
||||
// If we're already on an embed page, use current URL format
|
||||
const currentUrl = new URL(window.location.href);
|
||||
currentUrl.searchParams.set('m', mediaId);
|
||||
return currentUrl.toString();
|
||||
}
|
||||
|
||||
// Default embed URL format
|
||||
return `${origin}/embed?m=${mediaId}`;
|
||||
};
|
||||
|
||||
// Copy video URL to clipboard
|
||||
const handleCopyVideoUrl = async () => {
|
||||
const mediaId = getMediaId();
|
||||
const origin = getBaseOrigin();
|
||||
const videoUrl = `${origin}/view?m=${mediaId}`;
|
||||
|
||||
// Show copy icon
|
||||
if (customComponents.current?.seekIndicator) {
|
||||
customComponents.current.seekIndicator.show('copy-url');
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(videoUrl);
|
||||
closeContextMenu();
|
||||
// You can add a notification here if needed
|
||||
} catch (err) {
|
||||
console.error('Failed to copy video URL:', err);
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = videoUrl;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
closeContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// Copy video URL at current time to clipboard
|
||||
const handleCopyVideoUrlAtTime = async () => {
|
||||
if (!playerRef.current) {
|
||||
closeContextMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = Math.floor(playerRef.current.currentTime() || 0);
|
||||
const mediaId = getMediaId();
|
||||
const origin = getBaseOrigin();
|
||||
const videoUrl = `${origin}/view?m=${mediaId}&t=${currentTime}`;
|
||||
|
||||
// Show copy icon
|
||||
if (customComponents.current?.seekIndicator) {
|
||||
customComponents.current.seekIndicator.show('copy-url');
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(videoUrl);
|
||||
closeContextMenu();
|
||||
} catch (err) {
|
||||
console.error('Failed to copy video URL at time:', err);
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = videoUrl;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
closeContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// Copy embed code to clipboard
|
||||
const handleCopyEmbedCode = async () => {
|
||||
const embedUrl = getEmbedUrl();
|
||||
const embedCode = `<iframe width="560" height="315" src="${embedUrl}" frameborder="0" allowfullscreen></iframe>`;
|
||||
|
||||
// Show copy embed icon
|
||||
if (customComponents.current?.seekIndicator) {
|
||||
customComponents.current.seekIndicator.show('copy-embed');
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(embedCode);
|
||||
closeContextMenu();
|
||||
} catch (err) {
|
||||
console.error('Failed to copy embed code:', err);
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = embedCode;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
closeContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// Add context menu handler directly to video element and document (works before and after Video.js initialization)
|
||||
useEffect(() => {
|
||||
const videoElement = videoRef.current;
|
||||
|
||||
// Attach to document with capture to catch all contextmenu events, then filter
|
||||
const documentHandler = (e) => {
|
||||
// Check if the event originated from within the video player
|
||||
const target = e.target;
|
||||
const playerWrapper =
|
||||
videoElement?.closest('.video-js') || document.querySelector(`#${videoId}`)?.closest('.video-js');
|
||||
|
||||
if (playerWrapper && (playerWrapper.contains(target) || target === playerWrapper)) {
|
||||
handleContextMenu(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Use capture phase on document to catch before anything else
|
||||
document.addEventListener('contextmenu', documentHandler, true);
|
||||
|
||||
// Also attach directly to video element
|
||||
if (videoElement) {
|
||||
videoElement.addEventListener('contextmenu', handleContextMenu, true);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('contextmenu', documentHandler, true);
|
||||
if (videoElement) {
|
||||
videoElement.removeEventListener('contextmenu', handleContextMenu, true);
|
||||
}
|
||||
};
|
||||
}, [handleContextMenu, videoId]);
|
||||
|
||||
useEffect(() => {
|
||||
// Only initialize if we don't already have a player and element exists
|
||||
if (videoRef.current && !playerRef.current) {
|
||||
@@ -1078,6 +1300,9 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
currentVideo,
|
||||
relatedVideos,
|
||||
goToNextVideo,
|
||||
showRelated: finalShowRelated,
|
||||
showUserAvatar: finalShowUserAvatar,
|
||||
linkTitle: finalLinkTitle,
|
||||
});
|
||||
customComponents.current.endScreenHandler = endScreenHandler; // Store for cleanup
|
||||
|
||||
@@ -1098,8 +1323,8 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
}
|
||||
|
||||
// Handle URL timestamp parameter
|
||||
if (mediaData.urlTimestamp !== null && mediaData.urlTimestamp >= 0) {
|
||||
const timestamp = mediaData.urlTimestamp;
|
||||
if (finalTimestamp !== null && finalTimestamp >= 0) {
|
||||
const timestamp = finalTimestamp;
|
||||
|
||||
// Wait for video metadata to be loaded before seeking
|
||||
if (playerRef.current.readyState() >= 1) {
|
||||
@@ -1997,6 +2222,10 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
authorThumbnail: currentVideo.author_thumbnail,
|
||||
videoTitle: currentVideo.title,
|
||||
videoUrl: currentVideo.url,
|
||||
showTitle: finalShowTitle,
|
||||
showRelated: finalShowRelated,
|
||||
showUserAvatar: finalShowUserAvatar,
|
||||
linkTitle: finalLinkTitle,
|
||||
});
|
||||
}
|
||||
// END: Add Embed Info Overlay Component
|
||||
@@ -2083,52 +2312,113 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
// Make the video element focusable
|
||||
const videoElement = playerRef.current.el();
|
||||
videoElement.setAttribute('tabindex', '0');
|
||||
videoElement.focus();
|
||||
|
||||
if (!isEmbedPlayer) {
|
||||
videoElement.focus();
|
||||
}
|
||||
|
||||
// Add context menu (right-click) handler to the player wrapper and video element
|
||||
// Attach to player wrapper (this catches all clicks on the player)
|
||||
videoElement.addEventListener('contextmenu', handleContextMenu, true);
|
||||
|
||||
// Also try to attach to the actual video tech element
|
||||
const attachContextMenu = () => {
|
||||
const techElement =
|
||||
playerRef.current.el().querySelector('.vjs-tech') ||
|
||||
playerRef.current.el().querySelector('video') ||
|
||||
(playerRef.current.tech() && playerRef.current.tech().el());
|
||||
|
||||
if (techElement && techElement !== videoRef.current && techElement !== videoElement) {
|
||||
// Use capture phase to catch before Video.js might prevent it
|
||||
techElement.addEventListener('contextmenu', handleContextMenu, true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Try to attach immediately
|
||||
attachContextMenu();
|
||||
|
||||
// Also try after a short delay in case elements aren't ready yet
|
||||
setTimeout(() => {
|
||||
attachContextMenu();
|
||||
}, 100);
|
||||
|
||||
// Also try when video is loaded
|
||||
playerRef.current.one('loadedmetadata', () => {
|
||||
attachContextMenu();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
//}, 0);
|
||||
}
|
||||
|
||||
// Cleanup: Remove context menu event listener
|
||||
return () => {
|
||||
if (playerRef.current && playerRef.current.el()) {
|
||||
const playerEl = playerRef.current.el();
|
||||
playerEl.removeEventListener('contextmenu', handleContextMenu, true);
|
||||
|
||||
const techElement =
|
||||
playerEl.querySelector('.vjs-tech') ||
|
||||
playerEl.querySelector('video') ||
|
||||
(playerRef.current.tech() && playerRef.current.tech().el());
|
||||
if (techElement) {
|
||||
techElement.removeEventListener('contextmenu', handleContextMenu, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
id={videoId}
|
||||
controls={true}
|
||||
className={`video-js vjs-fluid vjs-default-skin${currentVideo.useRoundedCorners ? ' video-js-rounded-corners' : ''}`}
|
||||
preload="auto"
|
||||
poster={currentVideo.poster}
|
||||
tabIndex="0"
|
||||
>
|
||||
{/* <source src="/videos/sample-video.mp4" type="video/mp4" />
|
||||
<source src="/videos/sample-video.webm" type="video/webm" /> */}
|
||||
<p className="vjs-no-js">
|
||||
To view this video please enable JavaScript, and consider upgrading to a web browser that
|
||||
<a href="https://videojs.com/html5-video-support/" target="_blank">
|
||||
supports HTML5 video
|
||||
</a>
|
||||
</p>
|
||||
<>
|
||||
<video
|
||||
ref={videoRef}
|
||||
id={videoId}
|
||||
controls={true}
|
||||
className={`video-js ${isEmbedPlayer ? 'vjs-fill' : 'vjs-fluid'} vjs-default-skin${currentVideo.useRoundedCorners ? ' video-js-rounded-corners' : ''}`}
|
||||
preload="auto"
|
||||
poster={currentVideo.poster}
|
||||
tabIndex="0"
|
||||
>
|
||||
{/* <source src="/videos/sample-video.mp4" type="video/mp4" />
|
||||
<source src="/videos/sample-video.webm" type="video/webm" /> */}
|
||||
<p className="vjs-no-js">
|
||||
To view this video please enable JavaScript, and consider upgrading to a web browser that
|
||||
<a href="https://videojs.com/html5-video-support/" target="_blank">
|
||||
supports HTML5 video
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{/* Add subtitle tracks */}
|
||||
{/* {subtitleTracks &&
|
||||
subtitleTracks.map((track, index) => (
|
||||
<track
|
||||
key={index}
|
||||
kind={track.kind}
|
||||
src={track.src}
|
||||
srcLang={track.srclang}
|
||||
label={track.label}
|
||||
default={track.default}
|
||||
/>
|
||||
))} */}
|
||||
{/*
|
||||
<track kind="chapters" src="/sample-chapters.vtt" /> */}
|
||||
{/* Add chapters track */}
|
||||
{/* {chaptersData &&
|
||||
chaptersData.length > 0 &&
|
||||
(console.log('chaptersData', chaptersData), (<track kind="chapters" src="/sample-chapters.vtt" />))} */}
|
||||
</video>
|
||||
{/* Add subtitle tracks */}
|
||||
{/* {subtitleTracks &&
|
||||
subtitleTracks.map((track, index) => (
|
||||
<track
|
||||
key={index}
|
||||
kind={track.kind}
|
||||
src={track.src}
|
||||
srcLang={track.srclang}
|
||||
label={track.label}
|
||||
default={track.default}
|
||||
/>
|
||||
))} */}
|
||||
{/*
|
||||
<track kind="chapters" src="/sample-chapters.vtt" /> */}
|
||||
{/* Add chapters track */}
|
||||
{/* {chaptersData &&
|
||||
chaptersData.length > 0 &&
|
||||
(console.log('chaptersData', chaptersData), (<track kind="chapters" src="/sample-chapters.vtt" />))} */}
|
||||
</video>
|
||||
<VideoContextMenu
|
||||
visible={contextMenuVisible}
|
||||
position={contextMenuPosition}
|
||||
onClose={closeContextMenu}
|
||||
onCopyVideoUrl={handleCopyVideoUrl}
|
||||
onCopyVideoUrlAtTime={handleCopyVideoUrlAtTime}
|
||||
onCopyEmbedCode={handleCopyEmbedCode}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,17 @@ export class EndScreenHandler {
|
||||
}
|
||||
|
||||
handleVideoEnded() {
|
||||
const { isEmbedPlayer, userPreferences, mediaData, currentVideo, relatedVideos, goToNextVideo } = this.options;
|
||||
const {
|
||||
isEmbedPlayer,
|
||||
userPreferences,
|
||||
mediaData,
|
||||
currentVideo,
|
||||
relatedVideos,
|
||||
goToNextVideo,
|
||||
showRelated,
|
||||
showUserAvatar,
|
||||
linkTitle,
|
||||
} = this.options;
|
||||
|
||||
// For embed players, show big play button when video ends
|
||||
if (isEmbedPlayer) {
|
||||
@@ -73,6 +83,34 @@ export class EndScreenHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// If showRelated is false, we don't show the end screen or autoplay countdown
|
||||
if (showRelated === false) {
|
||||
// But we still want to keep the control bar visible and hide the poster
|
||||
setTimeout(() => {
|
||||
if (this.player && !this.player.isDisposed()) {
|
||||
const playerEl = this.player.el();
|
||||
if (playerEl) {
|
||||
// Hide poster elements
|
||||
const posterElements = playerEl.querySelectorAll('.vjs-poster');
|
||||
posterElements.forEach((posterEl) => {
|
||||
posterEl.style.display = 'none';
|
||||
posterEl.style.visibility = 'hidden';
|
||||
posterEl.style.opacity = '0';
|
||||
});
|
||||
|
||||
// Keep control bar visible
|
||||
const controlBar = this.player.getChild('controlBar');
|
||||
if (controlBar) {
|
||||
controlBar.show();
|
||||
controlBar.el().style.opacity = '1';
|
||||
controlBar.el().style.pointerEvents = 'auto';
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep controls active after video ends
|
||||
setTimeout(() => {
|
||||
if (this.player && !this.player.isDisposed()) {
|
||||
|
||||
5
frontend/.vscode/settings.json
vendored
5
frontend/.vscode/settings.json
vendored
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
"editor.formatOnSave": true,
|
||||
"prettier.configPath": "../.prettierrc"
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@ module.exports = {
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
},
|
||||
collectCoverageFrom: ['src/**'],
|
||||
collectCoverageFrom: ['src/**', '!src/static/lib/**'],
|
||||
};
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
"@babel/core": "^7.26.9",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@types/flux": "^3.1.15",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
|
||||
@@ -31,8 +31,11 @@ const VideoJSEmbed = ({
|
||||
poster,
|
||||
previewSprite,
|
||||
subtitlesInfo,
|
||||
enableAutoplay,
|
||||
inEmbed,
|
||||
showTitle,
|
||||
showRelated,
|
||||
showUserAvatar,
|
||||
linkTitle,
|
||||
hasTheaterMode,
|
||||
hasNextLink,
|
||||
nextLink,
|
||||
@@ -62,8 +65,10 @@ const VideoJSEmbed = ({
|
||||
if (typeof window !== 'undefined') {
|
||||
// Get URL parameters for autoplay, muted, and timestamp
|
||||
const urlTimestamp = getUrlParameter('t');
|
||||
const urlAutoplay = getUrlParameter('autoplay');
|
||||
const urlMuted = getUrlParameter('muted');
|
||||
const urlShowRelated = getUrlParameter('showRelated');
|
||||
const urlShowUserAvatar = getUrlParameter('showUserAvatar');
|
||||
const urlLinkTitle = getUrlParameter('linkTitle');
|
||||
|
||||
window.MEDIA_DATA = {
|
||||
data: data || {},
|
||||
@@ -71,7 +76,7 @@ const VideoJSEmbed = ({
|
||||
version: version,
|
||||
isPlayList: isPlayList,
|
||||
playerVolume: playerVolume || 0.5,
|
||||
playerSoundMuted: playerSoundMuted || (urlMuted === '1'),
|
||||
playerSoundMuted: urlMuted === '1',
|
||||
videoQuality: videoQuality || 'auto',
|
||||
videoPlaybackSpeed: videoPlaybackSpeed || 1,
|
||||
inTheaterMode: inTheaterMode || false,
|
||||
@@ -83,8 +88,11 @@ const VideoJSEmbed = ({
|
||||
poster: poster || '',
|
||||
previewSprite: previewSprite || null,
|
||||
subtitlesInfo: subtitlesInfo || [],
|
||||
enableAutoplay: enableAutoplay || (urlAutoplay === '1'),
|
||||
inEmbed: inEmbed || false,
|
||||
showTitle: showTitle || false,
|
||||
showRelated: showRelated !== undefined ? showRelated : (urlShowRelated === '1' || urlShowRelated === 'true' || urlShowRelated === null),
|
||||
showUserAvatar: showUserAvatar !== undefined ? showUserAvatar : (urlShowUserAvatar === '1' || urlShowUserAvatar === 'true' || urlShowUserAvatar === null),
|
||||
linkTitle: linkTitle !== undefined ? linkTitle : (urlLinkTitle === '1' || urlLinkTitle === 'true' || urlLinkTitle === null),
|
||||
hasTheaterMode: hasTheaterMode || false,
|
||||
hasNextLink: hasNextLink || false,
|
||||
nextLink: nextLink || null,
|
||||
@@ -92,8 +100,10 @@ const VideoJSEmbed = ({
|
||||
errorMessage: errorMessage || '',
|
||||
// URL parameters
|
||||
urlTimestamp: urlTimestamp ? parseInt(urlTimestamp, 10) : null,
|
||||
urlAutoplay: urlAutoplay === '1',
|
||||
urlMuted: urlMuted === '1',
|
||||
urlShowRelated: urlShowRelated === '1' || urlShowRelated === 'true',
|
||||
urlShowUserAvatar: urlShowUserAvatar === '1' || urlShowUserAvatar === 'true',
|
||||
urlLinkTitle: urlLinkTitle === '1' || urlLinkTitle === 'true',
|
||||
onClickNextCallback: onClickNextCallback || null,
|
||||
onClickPreviousCallback: onClickPreviousCallback || null,
|
||||
onStateUpdateCallback: onStateUpdateCallback || null,
|
||||
@@ -176,11 +186,17 @@ const VideoJSEmbed = ({
|
||||
// Scroll to the video player with smooth behavior
|
||||
const videoElement = document.querySelector(inEmbedRef.current ? '#video-embed' : '#video-main');
|
||||
if (videoElement) {
|
||||
videoElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest'
|
||||
});
|
||||
const urlScroll = getUrlParameter('scroll');
|
||||
const isIframe = window.parent !== window;
|
||||
|
||||
// Only scroll if not in an iframe, OR if explicitly requested via scroll=1 parameter
|
||||
if (!isIframe || urlScroll === '1' || urlScroll === 'true') {
|
||||
videoElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest'
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('VideoJS player not found for timestamp navigation');
|
||||
@@ -220,7 +236,14 @@ const VideoJSEmbed = ({
|
||||
|
||||
return (
|
||||
<div className="video-js-wrapper" ref={containerRef}>
|
||||
{inEmbed ? <div id="video-js-root-embed" className="video-js-root-embed" /> : <div id="video-js-root-main" className="video-js-root-main" />}
|
||||
{inEmbed ? (
|
||||
<div
|
||||
id="video-js-root-embed"
|
||||
className="video-js-root-embed"
|
||||
/>
|
||||
) : (
|
||||
<div id="video-js-root-main" className="video-js-root-main" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,10 +4,32 @@ import { LinksContext, SiteConsumer } from '../../utils/contexts/';
|
||||
import { PageStore, MediaPageStore } from '../../utils/stores/';
|
||||
import { PageActions, MediaPageActions } from '../../utils/actions/';
|
||||
import { CircleIconButton, MaterialIcon, NumericInputWithUnit } from '../_shared/';
|
||||
import VideoViewer from '../media-viewer/VideoViewer';
|
||||
|
||||
const EMBED_OPTIONS_STORAGE_KEY = 'mediacms_embed_options';
|
||||
|
||||
function loadEmbedOptions() {
|
||||
try {
|
||||
const saved = localStorage.getItem(EMBED_OPTIONS_STORAGE_KEY);
|
||||
if (saved) {
|
||||
return JSON.parse(saved);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveEmbedOptions(options) {
|
||||
try {
|
||||
localStorage.setItem(EMBED_OPTIONS_STORAGE_KEY, JSON.stringify(options));
|
||||
} catch (e) {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
}
|
||||
|
||||
export function MediaShareEmbed(props) {
|
||||
const embedVideoDimensions = PageStore.get('config-options').embedded.video.dimensions;
|
||||
const savedOptions = loadEmbedOptions();
|
||||
|
||||
const links = useContext(LinksContext);
|
||||
|
||||
@@ -18,12 +40,19 @@ export function MediaShareEmbed(props) {
|
||||
const onRightBottomRef = useRef(null);
|
||||
|
||||
const [maxHeight, setMaxHeight] = useState(window.innerHeight - 144 + 56);
|
||||
const [keepAspectRatio, setKeepAspectRatio] = useState(false);
|
||||
const [aspectRatio, setAspectRatio] = useState('16:9');
|
||||
const [embedWidthValue, setEmbedWidthValue] = useState(embedVideoDimensions.width);
|
||||
const [embedWidthUnit, setEmbedWidthUnit] = useState(embedVideoDimensions.widthUnit);
|
||||
const [embedHeightValue, setEmbedHeightValue] = useState(embedVideoDimensions.height);
|
||||
const [embedHeightUnit, setEmbedHeightUnit] = useState(embedVideoDimensions.heightUnit);
|
||||
const [keepAspectRatio, setKeepAspectRatio] = useState(savedOptions?.keepAspectRatio ?? true);
|
||||
const [showTitle, setShowTitle] = useState(savedOptions?.showTitle ?? true);
|
||||
const [showRelated, setShowRelated] = useState(savedOptions?.showRelated ?? true);
|
||||
const [showUserAvatar, setShowUserAvatar] = useState(savedOptions?.showUserAvatar ?? true);
|
||||
const [linkTitle, setLinkTitle] = useState(savedOptions?.linkTitle ?? true);
|
||||
const [responsive, setResponsive] = useState(savedOptions?.responsive ?? false);
|
||||
const [startAt, setStartAt] = useState(false);
|
||||
const [startTime, setStartTime] = useState('0:00');
|
||||
const [aspectRatio, setAspectRatio] = useState(savedOptions?.aspectRatio ?? '16:9');
|
||||
const [embedWidthValue, setEmbedWidthValue] = useState(savedOptions?.embedWidthValue ?? embedVideoDimensions.width);
|
||||
const [embedWidthUnit, setEmbedWidthUnit] = useState(savedOptions?.embedWidthUnit ?? embedVideoDimensions.widthUnit);
|
||||
const [embedHeightValue, setEmbedHeightValue] = useState(savedOptions?.embedHeightValue ?? embedVideoDimensions.height);
|
||||
const [embedHeightUnit, setEmbedHeightUnit] = useState(savedOptions?.embedHeightUnit ?? embedVideoDimensions.heightUnit);
|
||||
const [rightMiddlePositionTop, setRightMiddlePositionTop] = useState(60);
|
||||
const [rightMiddlePositionBottom, setRightMiddlePositionBottom] = useState(60);
|
||||
const [unitOptions, setUnitOptions] = useState([
|
||||
@@ -71,36 +100,65 @@ export function MediaShareEmbed(props) {
|
||||
setEmbedHeightUnit(newVal);
|
||||
}
|
||||
|
||||
function onKeepAspectRatioChange() {
|
||||
const newVal = !keepAspectRatio;
|
||||
function onShowTitleChange() {
|
||||
setShowTitle(!showTitle);
|
||||
}
|
||||
|
||||
const arr = aspectRatio.split(':');
|
||||
const x = arr[0];
|
||||
const y = arr[1];
|
||||
function onShowRelatedChange() {
|
||||
setShowRelated(!showRelated);
|
||||
}
|
||||
|
||||
setKeepAspectRatio(newVal);
|
||||
setEmbedWidthUnit(newVal ? 'px' : embedWidthUnit);
|
||||
setEmbedHeightUnit(newVal ? 'px' : embedHeightUnit);
|
||||
setEmbedHeightValue(newVal ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue);
|
||||
setUnitOptions(
|
||||
newVal
|
||||
? [{ key: 'px', label: 'px' }]
|
||||
: [
|
||||
{ key: 'px', label: 'px' },
|
||||
{ key: 'percent', label: '%' },
|
||||
]
|
||||
);
|
||||
function onShowUserAvatarChange() {
|
||||
setShowUserAvatar(!showUserAvatar);
|
||||
}
|
||||
|
||||
function onLinkTitleChange() {
|
||||
setLinkTitle(!linkTitle);
|
||||
}
|
||||
|
||||
function onResponsiveChange() {
|
||||
const nextResponsive = !responsive;
|
||||
setResponsive(nextResponsive);
|
||||
|
||||
if (!nextResponsive) {
|
||||
if (aspectRatio !== 'custom') {
|
||||
const arr = aspectRatio.split(':');
|
||||
const x = arr[0];
|
||||
const y = arr[1];
|
||||
|
||||
setKeepAspectRatio(true);
|
||||
setEmbedHeightValue(parseInt((embedWidthValue * y) / x, 10));
|
||||
} else {
|
||||
setKeepAspectRatio(false);
|
||||
}
|
||||
} else {
|
||||
setKeepAspectRatio(false);
|
||||
}
|
||||
}
|
||||
|
||||
function onStartAtChange() {
|
||||
setStartAt(!startAt);
|
||||
}
|
||||
|
||||
function onStartTimeChange(e) {
|
||||
setStartTime(e.target.value);
|
||||
}
|
||||
|
||||
function onAspectRatioChange() {
|
||||
const newVal = aspectRatioValueRef.current.value;
|
||||
|
||||
const arr = newVal.split(':');
|
||||
const x = arr[0];
|
||||
const y = arr[1];
|
||||
if (newVal === 'custom') {
|
||||
setAspectRatio(newVal);
|
||||
setKeepAspectRatio(false);
|
||||
} else {
|
||||
const arr = newVal.split(':');
|
||||
const x = arr[0];
|
||||
const y = arr[1];
|
||||
|
||||
setAspectRatio(newVal);
|
||||
setEmbedHeightValue(keepAspectRatio ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue);
|
||||
setAspectRatio(newVal);
|
||||
setKeepAspectRatio(true);
|
||||
setEmbedHeightValue(parseInt((embedWidthValue * y) / x, 10));
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
@@ -130,13 +188,88 @@ export function MediaShareEmbed(props) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Save embed options to localStorage when they change (except startAt/startTime)
|
||||
useEffect(() => {
|
||||
saveEmbedOptions({
|
||||
showTitle,
|
||||
showRelated,
|
||||
showUserAvatar,
|
||||
linkTitle,
|
||||
responsive,
|
||||
aspectRatio,
|
||||
embedWidthValue,
|
||||
embedWidthUnit,
|
||||
embedHeightValue,
|
||||
embedHeightUnit,
|
||||
keepAspectRatio,
|
||||
});
|
||||
}, [showTitle, showRelated, showUserAvatar, linkTitle, responsive, aspectRatio, embedWidthValue, embedWidthUnit, embedHeightValue, embedHeightUnit, keepAspectRatio]);
|
||||
|
||||
function getEmbedCode() {
|
||||
const mediaId = MediaPageStore.get('media-id');
|
||||
const params = new URLSearchParams();
|
||||
if (showTitle) params.set('showTitle', '1');
|
||||
else params.set('showTitle', '0');
|
||||
|
||||
if (showRelated) params.set('showRelated', '1');
|
||||
else params.set('showRelated', '0');
|
||||
|
||||
if (showUserAvatar) params.set('showUserAvatar', '1');
|
||||
else params.set('showUserAvatar', '0');
|
||||
|
||||
if (linkTitle) params.set('linkTitle', '1');
|
||||
else params.set('linkTitle', '0');
|
||||
|
||||
if (startAt && startTime) {
|
||||
const parts = startTime.split(':').reverse();
|
||||
let seconds = 0;
|
||||
if (parts[0]) seconds += parseInt(parts[0], 10) || 0;
|
||||
if (parts[1]) seconds += (parseInt(parts[1], 10) || 0) * 60;
|
||||
if (parts[2]) seconds += (parseInt(parts[2], 10) || 0) * 3600;
|
||||
if (seconds > 0) params.set('t', seconds);
|
||||
}
|
||||
|
||||
const separator = links.embed.includes('?') ? '&' : '?';
|
||||
const finalUrl = `${links.embed}${mediaId}${separator}${params.toString()}`;
|
||||
|
||||
if (responsive) {
|
||||
if (aspectRatio === 'custom') {
|
||||
// Use current width/height values to calculate aspect ratio for custom
|
||||
const ratio = `${embedWidthValue} / ${embedHeightValue}`;
|
||||
const maxWidth = `calc(100vh * ${embedWidthValue} / ${embedHeightValue})`;
|
||||
return `<iframe src="${finalUrl}" style="width:100%;max-width:${maxWidth};aspect-ratio:${ratio};display:block;margin:auto;border:0;" allowFullScreen></iframe>`;
|
||||
}
|
||||
const arr = aspectRatio.split(':');
|
||||
const ratio = `${arr[0]} / ${arr[1]}`;
|
||||
const maxWidth = `calc(100vh * ${arr[0]} / ${arr[1]})`;
|
||||
return `<iframe src="${finalUrl}" style="width:100%;max-width:${maxWidth};aspect-ratio:${ratio};display:block;margin:auto;border:0;" allowFullScreen></iframe>`;
|
||||
}
|
||||
|
||||
const width = 'percent' === embedWidthUnit ? embedWidthValue + '%' : embedWidthValue;
|
||||
const height = 'percent' === embedHeightUnit ? embedHeightValue + '%' : embedHeightValue;
|
||||
return `<iframe width="${width}" height="${height}" src="${finalUrl}" frameBorder="0" allowFullScreen></iframe>`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="share-embed" style={{ maxHeight: maxHeight + 'px' }}>
|
||||
<div className="share-embed-inner">
|
||||
<div className="on-left">
|
||||
<div className="media-embed-wrap">
|
||||
<SiteConsumer>
|
||||
{(site) => <VideoViewer data={MediaPageStore.get('media-data')} siteUrl={site.url} inEmbed={true} />}
|
||||
{(site) => {
|
||||
const previewUrl = `${links.embed + MediaPageStore.get('media-id')}&showTitle=${showTitle ? '1' : '0'}&showRelated=${showRelated ? '1' : '0'}&showUserAvatar=${showUserAvatar ? '1' : '0'}&linkTitle=${linkTitle ? '1' : '0'}${startAt ? '&t=' + (startTime.split(':').reverse().reduce((acc, cur, i) => acc + (parseInt(cur, 10) || 0) * Math.pow(60, i), 0)) : ''}`;
|
||||
|
||||
const style = {};
|
||||
style.width = '100%';
|
||||
style.height = '480px';
|
||||
style.overflow = 'hidden';
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<iframe width="100%" height="100%" src={previewUrl} frameBorder="0" allowFullScreen></iframe>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</SiteConsumer>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,16 +291,7 @@ export function MediaShareEmbed(props) {
|
||||
>
|
||||
<textarea
|
||||
readOnly
|
||||
value={
|
||||
'<iframe width="' +
|
||||
('percent' === embedWidthUnit ? embedWidthValue + '%' : embedWidthValue) +
|
||||
'" height="' +
|
||||
('percent' === embedHeightUnit ? embedHeightValue + '%' : embedHeightValue) +
|
||||
'" src="' +
|
||||
links.embed +
|
||||
MediaPageStore.get('media-id') +
|
||||
'" frameborder="0" allowfullscreen></iframe>'
|
||||
}
|
||||
value={getEmbedCode()}
|
||||
></textarea>
|
||||
|
||||
<div className="iframe-config">
|
||||
@@ -179,59 +303,106 @@ export function MediaShareEmbed(props) {
|
||||
</div>*/}
|
||||
|
||||
<div className="option-content">
|
||||
<div className="ratio-options">
|
||||
<div className="ratio-options" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 10px' }}>
|
||||
<div className="options-group">
|
||||
<label style={{ minHeight: '36px' }}>
|
||||
<input type="checkbox" checked={keepAspectRatio} onChange={onKeepAspectRatioChange} />
|
||||
Keep aspect ratio
|
||||
<label style={{ minHeight: '36px', whiteSpace: 'nowrap' }}>
|
||||
<input type="checkbox" checked={showTitle} onChange={onShowTitleChange} />
|
||||
Show title
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!keepAspectRatio ? null : (
|
||||
<div className="options-group">
|
||||
<select ref={aspectRatioValueRef} onChange={onAspectRatioChange} value={aspectRatio}>
|
||||
<optgroup label="Horizontal orientation">
|
||||
<option value="16:9">16:9</option>
|
||||
<option value="4:3">4:3</option>
|
||||
<option value="3:2">3:2</option>
|
||||
</optgroup>
|
||||
<optgroup label="Vertical orientation">
|
||||
<option value="9:16">9:16</option>
|
||||
<option value="3:4">3:4</option>
|
||||
<option value="2:3">2:3</option>
|
||||
</optgroup>
|
||||
<div className="options-group">
|
||||
<label style={{ minHeight: '36px', whiteSpace: 'nowrap', opacity: showTitle ? 1 : 0.5 }}>
|
||||
<input type="checkbox" checked={linkTitle} onChange={onLinkTitleChange} disabled={!showTitle} />
|
||||
Link title
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="options-group">
|
||||
<label style={{ minHeight: '36px', whiteSpace: 'nowrap' }}>
|
||||
<input type="checkbox" checked={showRelated} onChange={onShowRelatedChange} />
|
||||
Show related
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="options-group">
|
||||
<label style={{ minHeight: '36px', whiteSpace: 'nowrap', opacity: showTitle ? 1 : 0.5 }}>
|
||||
<input type="checkbox" checked={showUserAvatar} onChange={onShowUserAvatarChange} disabled={!showTitle} />
|
||||
Show user avatar
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="options-group" style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<label style={{ minHeight: '36px', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', marginRight: '10px' }}>
|
||||
<input type="checkbox" checked={responsive} onChange={onResponsiveChange} />
|
||||
Responsive
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="options-group" style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<label style={{ minHeight: '36px', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', marginRight: '10px' }}>
|
||||
<input type="checkbox" checked={startAt} onChange={onStartAtChange} />
|
||||
Start at
|
||||
</label>
|
||||
{startAt && (
|
||||
<input
|
||||
type="text"
|
||||
value={startTime}
|
||||
onChange={onStartTimeChange}
|
||||
style={{ width: '60px', height: '28px', fontSize: '12px', padding: '2px 5px' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="options-group" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
<div style={{ fontSize: '12px', marginBottom: '4px', color: 'rgba(0,0,0,0.6)' }}>Aspect Ratio</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<select
|
||||
ref={aspectRatioValueRef}
|
||||
onChange={onAspectRatioChange}
|
||||
value={aspectRatio}
|
||||
style={{ height: '28px', fontSize: '12px' }}
|
||||
>
|
||||
<option value="16:9">16:9</option>
|
||||
<option value="4:3">4:3</option>
|
||||
<option value="3:2">3:2</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="options-group">
|
||||
<NumericInputWithUnit
|
||||
valueCallback={onEmbedWidthValueChange}
|
||||
unitCallback={onEmbedWidthUnitChange}
|
||||
label={'Width'}
|
||||
defaultValue={parseInt(embedWidthValue, 10)}
|
||||
defaultUnit={embedWidthUnit}
|
||||
minValue={1}
|
||||
maxValue={99999}
|
||||
units={unitOptions}
|
||||
/>
|
||||
</div>
|
||||
{!responsive && (
|
||||
<>
|
||||
<div className="options-group">
|
||||
<NumericInputWithUnit
|
||||
valueCallback={onEmbedWidthValueChange}
|
||||
unitCallback={onEmbedWidthUnitChange}
|
||||
label={'Width'}
|
||||
defaultValue={parseInt(embedWidthValue, 10)}
|
||||
defaultUnit={embedWidthUnit}
|
||||
minValue={1}
|
||||
maxValue={99999}
|
||||
units={unitOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="options-group">
|
||||
<NumericInputWithUnit
|
||||
valueCallback={onEmbedHeightValueChange}
|
||||
unitCallback={onEmbedHeightUnitChange}
|
||||
label={'Height'}
|
||||
defaultValue={parseInt(embedHeightValue, 10)}
|
||||
defaultUnit={embedHeightUnit}
|
||||
minValue={1}
|
||||
maxValue={99999}
|
||||
units={unitOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="options-group">
|
||||
<NumericInputWithUnit
|
||||
valueCallback={onEmbedHeightValueChange}
|
||||
unitCallback={onEmbedHeightUnitChange}
|
||||
label={'Height'}
|
||||
defaultValue={parseInt(embedHeightValue, 10)}
|
||||
defaultUnit={embedHeightUnit}
|
||||
minValue={1}
|
||||
maxValue={99999}
|
||||
units={unitOptions}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1930,9 +1930,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.media-embed-wrap {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
.media-embed-wrap {
|
||||
display: block;
|
||||
|
||||
.player-container,
|
||||
.player-container-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 0;
|
||||
background: #000;
|
||||
}
|
||||
.player-container,
|
||||
.player-container-inner {
|
||||
width: 100%;
|
||||
@@ -1946,6 +1958,10 @@
|
||||
.circle-icon-button {
|
||||
}
|
||||
|
||||
.video-js.vjs-mediacms {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
.video-js.vjs-mediacms {
|
||||
padding-top: math.div(9, 16) * 100%;
|
||||
}
|
||||
|
||||
@@ -410,8 +410,12 @@ export default class VideoViewer extends React.PureComponent {
|
||||
poster: this.videoPoster,
|
||||
previewSprite: previewSprite,
|
||||
subtitlesInfo: this.props.data.subtitles_info,
|
||||
enableAutoplay: !this.props.inEmbed,
|
||||
inEmbed: this.props.inEmbed,
|
||||
showTitle: this.props.showTitle,
|
||||
showRelated: this.props.showRelated,
|
||||
showUserAvatar: this.props.showUserAvatar,
|
||||
linkTitle: this.props.linkTitle,
|
||||
urlTimestamp: this.props.timestamp,
|
||||
hasTheaterMode: !this.props.inEmbed,
|
||||
hasNextLink: !!nextLink,
|
||||
nextLink: nextLink,
|
||||
@@ -435,9 +439,19 @@ export default class VideoViewer extends React.PureComponent {
|
||||
|
||||
VideoViewer.defaultProps = {
|
||||
inEmbed: !0,
|
||||
showTitle: !0,
|
||||
showRelated: !0,
|
||||
showUserAvatar: !0,
|
||||
linkTitle: !0,
|
||||
timestamp: null,
|
||||
siteUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
VideoViewer.propTypes = {
|
||||
inEmbed: PropTypes.bool,
|
||||
showTitle: PropTypes.bool,
|
||||
showRelated: PropTypes.bool,
|
||||
showUserAvatar: PropTypes.bool,
|
||||
linkTitle: PropTypes.bool,
|
||||
timestamp: PropTypes.number,
|
||||
};
|
||||
@@ -41,7 +41,7 @@ export const EmbedPage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="embed-wrap" style={wrapperStyles}>
|
||||
<div className="embed-wrap media-embed-wrap" style={wrapperStyles}>
|
||||
{failedMediaLoad && (
|
||||
<div className="player-container player-container-error" style={containerStyles}>
|
||||
<div className="player-container-inner" style={containerStyles}>
|
||||
@@ -59,9 +59,32 @@ export const EmbedPage: React.FC = () => {
|
||||
|
||||
{loadedVideo && (
|
||||
<SiteConsumer>
|
||||
{(site) => (
|
||||
<VideoViewer data={MediaPageStore.get('media-data')} siteUrl={site.url} containerStyles={containerStyles} />
|
||||
)}
|
||||
{(site) => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const urlShowTitle = urlParams.get('showTitle');
|
||||
const showTitle = urlShowTitle !== '0';
|
||||
const urlShowRelated = urlParams.get('showRelated');
|
||||
const showRelated = urlShowRelated !== '0';
|
||||
const urlShowUserAvatar = urlParams.get('showUserAvatar');
|
||||
const showUserAvatar = urlShowUserAvatar !== '0';
|
||||
const urlLinkTitle = urlParams.get('linkTitle');
|
||||
const linkTitle = urlLinkTitle !== '0';
|
||||
const urlTimestamp = urlParams.get('t');
|
||||
const timestamp = urlTimestamp ? parseInt(urlTimestamp, 10) : null;
|
||||
|
||||
return (
|
||||
<VideoViewer
|
||||
data={MediaPageStore.get('media-data')}
|
||||
siteUrl={site.url}
|
||||
containerStyles={containerStyles}
|
||||
showTitle={showTitle}
|
||||
showRelated={showRelated}
|
||||
showUserAvatar={showUserAvatar}
|
||||
linkTitle={linkTitle}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</SiteConsumer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const HistoryPage: React.FC = () => {
|
||||
const anonymousPage = isAnonymous || !PageStore.get('config-options').pages.profile.includeHistory;
|
||||
|
||||
if (!anonymousPage) {
|
||||
addClassname(document.getElementById('page-history'), 'profile-page-history');
|
||||
addClassname(document.getElementById('page-history')!, 'profile-page-history');
|
||||
window.MediaCMS.profileId = username;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
<MediaListRow
|
||||
title={featured_title}
|
||||
style={!visibleFeatured ? { display: 'none' } : undefined}
|
||||
viewAllLink={featured_view_all_link ? links.featured : null}
|
||||
viewAllLink={featured_view_all_link ? links.featured : undefined}
|
||||
>
|
||||
<InlineSliderItemListAsync
|
||||
requestUrl={apiUrl.featured}
|
||||
@@ -93,7 +93,7 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
<MediaListRow
|
||||
title={recommended_title}
|
||||
style={!visibleRecommended ? { display: 'none' } : undefined}
|
||||
viewAllLink={recommended_view_all_link ? links.recommended : null}
|
||||
viewAllLink={recommended_view_all_link ? links.recommended : undefined}
|
||||
>
|
||||
<InlineSliderItemListAsync
|
||||
requestUrl={apiUrl.recommended}
|
||||
@@ -108,7 +108,7 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
<MediaListRow
|
||||
title={latest_title}
|
||||
style={!visibleLatest ? { display: 'none' } : undefined}
|
||||
viewAllLink={latest_view_all_link ? links.latest : null}
|
||||
viewAllLink={latest_view_all_link ? links.latest : undefined}
|
||||
>
|
||||
<ItemListAsync
|
||||
pageItems={30}
|
||||
|
||||
@@ -55,7 +55,7 @@ export const LikedMediaPage: React.FC = () => {
|
||||
const anonymousPage = isAnonymous || !PageStore.get('config-options').pages.profile.includeLikedMedia;
|
||||
|
||||
if (!anonymousPage) {
|
||||
addClassname(document.getElementById('page-liked'), 'profile-page-liked');
|
||||
addClassname(document.getElementById('page-liked')!, 'profile-page-liked');
|
||||
window.MediaCMS.profileId = username;
|
||||
}
|
||||
|
||||
|
||||
1
frontend/src/static/js/types/DeepPartial.ts
Normal file
1
frontend/src/static/js/types/DeepPartial.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
|
||||
212
frontend/src/static/js/types/GlobalMediaCMS.ts
Normal file
212
frontend/src/static/js/types/GlobalMediaCMS.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
type GlobalMediaCMSApi = {
|
||||
actions: string;
|
||||
categories: string;
|
||||
comments: string;
|
||||
history: string;
|
||||
liked: string;
|
||||
manage_comments: string;
|
||||
manage_media: string;
|
||||
manage_users: string;
|
||||
media: string;
|
||||
members: string;
|
||||
playlists: string;
|
||||
search: string;
|
||||
tags: string;
|
||||
};
|
||||
|
||||
type GlobalMediaCMSContents = {
|
||||
header: {
|
||||
right: string;
|
||||
onLogoRight: string;
|
||||
};
|
||||
notifications: {
|
||||
messages: {
|
||||
addToLiked: string;
|
||||
removeFromLiked: string;
|
||||
addToDisliked: string;
|
||||
removeFromDisliked: string;
|
||||
};
|
||||
};
|
||||
sidebar: {
|
||||
belowNavMenu: string;
|
||||
belowThemeSwitcher: string;
|
||||
footer: string;
|
||||
mainMenuExtraItems: { text: string; link: string; icon: string; className?: string }[]; // @todo: Check "className"
|
||||
navMenuItems: { text: string; link: string; icon: string; className?: string }[]; // @todo: Check "className"
|
||||
};
|
||||
uploader: {
|
||||
belowUploadArea: string;
|
||||
postUploadMessage: string;
|
||||
};
|
||||
};
|
||||
|
||||
type GlobalMediaCMSFeatures = {
|
||||
embeddedVideo: {
|
||||
initialDimensions: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
headerBar: {
|
||||
hideLogin: boolean;
|
||||
hideRegister: boolean;
|
||||
};
|
||||
sideBar: {
|
||||
hideHomeLink: boolean;
|
||||
hideTagsLink: boolean;
|
||||
hideCategoriesLink: boolean;
|
||||
};
|
||||
media: {
|
||||
actions: {
|
||||
share: boolean;
|
||||
report: boolean;
|
||||
like: boolean;
|
||||
dislike: boolean;
|
||||
download: boolean;
|
||||
comment: boolean;
|
||||
comment_mention: boolean;
|
||||
save: boolean;
|
||||
};
|
||||
shareOptions: ('embed' | 'email')[];
|
||||
};
|
||||
mediaItem: {
|
||||
hideDate: boolean;
|
||||
hideViews: boolean;
|
||||
hideAuthor: boolean;
|
||||
};
|
||||
playlists: {
|
||||
mediaTypes: ('audio' | 'video')[];
|
||||
};
|
||||
};
|
||||
|
||||
type GlobalCMSPages = {
|
||||
home: {
|
||||
sections: {
|
||||
latest: { title: string };
|
||||
featured: { title: string };
|
||||
recommended: { title: string };
|
||||
};
|
||||
};
|
||||
media: {
|
||||
categoriesWithTitle: boolean;
|
||||
htmlInDescription: boolean;
|
||||
hideViews: boolean;
|
||||
related: { initialSize: number };
|
||||
};
|
||||
profile: {
|
||||
htmlInDescription: boolean;
|
||||
includeHistory: boolean;
|
||||
includeLikedMedia: boolean;
|
||||
};
|
||||
search: { advancedFilters: boolean };
|
||||
};
|
||||
|
||||
type GlobalCMSSite = {
|
||||
api: string;
|
||||
devEnv: boolean;
|
||||
id: string;
|
||||
logo: {
|
||||
lightMode: { img: string; svg: string };
|
||||
darkMode: { img: string; svg: string };
|
||||
};
|
||||
pages: {
|
||||
featured: { enabled: boolean; title: string };
|
||||
latest: { enabled: boolean; title: string };
|
||||
members: { enabled: boolean; title: string };
|
||||
recommended: { enabled: boolean; title: string };
|
||||
};
|
||||
taxonomies: {
|
||||
categories: { enabled: boolean; title: string };
|
||||
tags: { enabled: boolean; title: string };
|
||||
};
|
||||
theme: {
|
||||
mode: 'light' | 'dark';
|
||||
switch: { enabled: boolean; position: 'header' | 'sidebar' };
|
||||
};
|
||||
title: string;
|
||||
url: string;
|
||||
useRoundedCorners: boolean;
|
||||
userPages: {
|
||||
history: { enabled: boolean; title: string };
|
||||
liked: { enabled: boolean; title: string };
|
||||
};
|
||||
version: string;
|
||||
};
|
||||
|
||||
type GlobalCMSUrl = {
|
||||
addMedia: string; // eg: "./add-media.html";
|
||||
admin: string; // eg: "/admin";
|
||||
categories: string; // eg: "./categories.html";
|
||||
changePassword: string; // eg: "./change-password.html";
|
||||
editChannel: string; // eg: "./edit-channel.html";
|
||||
editProfile: string; // eg: "./edit-profile.html";
|
||||
error404: string; // eg: "./error.html";
|
||||
featuredMedia: string; // eg: "./featured.html";
|
||||
history: string; // eg: "./history.html";
|
||||
home: string; // eg: "./index.html";
|
||||
latestMedia: string; // eg: "./latest.html";
|
||||
likedMedia: string; // eg: "./liked.html";
|
||||
manageComments: string; // eg: "./manage-comments.html";
|
||||
manageMedia: string; // eg: "./manage-media.html";
|
||||
manageUsers: string; // eg: "./manage-users.html";
|
||||
members: string; // eg: "./members.html";
|
||||
recommendedMedia: string; // eg: "./recommended.html";
|
||||
register: string; // eg: "./register.html";
|
||||
search: string; // eg: "./search.html";
|
||||
signin: string; // eg: "./signin.html";
|
||||
signout: string; // eg: "./signout.html";
|
||||
tags: string; // eg: "./tags.html";
|
||||
};
|
||||
|
||||
type GlobalCMSUser = {
|
||||
name: string;
|
||||
username: string;
|
||||
thumbnail: string;
|
||||
is: {
|
||||
admin: boolean;
|
||||
anonymous: boolean;
|
||||
};
|
||||
can: {
|
||||
// a
|
||||
addComment: boolean;
|
||||
addMedia: boolean;
|
||||
// c
|
||||
canSeeMembersPage: boolean;
|
||||
changePassword: boolean;
|
||||
contactUser: boolean;
|
||||
// d
|
||||
deleteComment: boolean;
|
||||
deleteMedia: boolean;
|
||||
deleteProfile: boolean;
|
||||
// e
|
||||
editMedia: boolean;
|
||||
editProfile: boolean;
|
||||
editSubtitle: boolean;
|
||||
// l
|
||||
// m
|
||||
manageComments: boolean;
|
||||
manageMedia: boolean;
|
||||
manageUsers: boolean;
|
||||
mentionComment: boolean;
|
||||
// r
|
||||
readComment: boolean;
|
||||
// u
|
||||
usersNeedsToBeApproved: boolean;
|
||||
};
|
||||
pages: {
|
||||
about: string;
|
||||
media: string;
|
||||
playlists: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type GlobalMediaCMS = {
|
||||
api: GlobalMediaCMSApi;
|
||||
contents: GlobalMediaCMSContents;
|
||||
features: GlobalMediaCMSFeatures;
|
||||
pages: GlobalCMSPages;
|
||||
profileId?: string;
|
||||
site: GlobalCMSSite;
|
||||
url: GlobalCMSUrl;
|
||||
user: GlobalCMSUser;
|
||||
};
|
||||
200
frontend/src/static/js/types/MediaCMSConfig.ts
Normal file
200
frontend/src/static/js/types/MediaCMSConfig.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { GlobalMediaCMS } from './GlobalMediaCMS';
|
||||
|
||||
type MediaCMSConfigApi = {
|
||||
archive: {
|
||||
tags: string;
|
||||
categories: string;
|
||||
};
|
||||
featured: string;
|
||||
manage: {
|
||||
media: string;
|
||||
users: string;
|
||||
comments: string;
|
||||
};
|
||||
media: string;
|
||||
playlists: string;
|
||||
recommended: string;
|
||||
search: {
|
||||
query: string;
|
||||
titles: string;
|
||||
tag: string;
|
||||
category: string;
|
||||
};
|
||||
user: {
|
||||
liked: string;
|
||||
history: string;
|
||||
playlists: string;
|
||||
};
|
||||
users: string; // @todo: "users" or "members"?
|
||||
};
|
||||
|
||||
type MediaCMSConfigContents = Omit<GlobalMediaCMS['contents'], 'notifications' | 'sidebar'> & {
|
||||
sidebar: {
|
||||
belowNavMenu: GlobalMediaCMS['contents']['sidebar']['belowNavMenu'];
|
||||
belowThemeSwitcher: GlobalMediaCMS['contents']['sidebar']['belowThemeSwitcher'];
|
||||
footer: GlobalMediaCMS['contents']['sidebar']['footer'];
|
||||
mainMenuExtra: { items: GlobalMediaCMS['contents']['sidebar']['mainMenuExtraItems'] };
|
||||
navMenu: { items: GlobalMediaCMS['contents']['sidebar']['navMenuItems'] };
|
||||
};
|
||||
};
|
||||
|
||||
type MediaCMSConfigEnabled = Pick<GlobalMediaCMS['site'], 'taxonomies'> & {
|
||||
pages: GlobalMediaCMS['site']['pages'] & GlobalMediaCMS['site']['userPages'];
|
||||
};
|
||||
|
||||
type MediaCMSConfigMember = {
|
||||
name: GlobalMediaCMS['user']['name'] | null;
|
||||
username: GlobalMediaCMS['user']['username'] | null;
|
||||
thumbnail: GlobalMediaCMS['user']['thumbnail'] | null;
|
||||
is: GlobalMediaCMS['user']['is'];
|
||||
can: {
|
||||
// a
|
||||
addComment: boolean;
|
||||
addMedia: boolean;
|
||||
// c
|
||||
canSeeMembersPage: boolean; // @note: This sould be renamed
|
||||
changePassword: boolean;
|
||||
contactUser: boolean;
|
||||
// d
|
||||
deleteComment: boolean;
|
||||
deleteMedia: boolean;
|
||||
deleteProfile: boolean;
|
||||
dislikeMedia: boolean;
|
||||
downloadMedia: boolean;
|
||||
// e
|
||||
editMedia: boolean;
|
||||
editProfile: boolean;
|
||||
editSubtitle: boolean;
|
||||
// l
|
||||
likeMedia: boolean;
|
||||
login: boolean;
|
||||
// m
|
||||
manageComments: boolean;
|
||||
manageMedia: boolean;
|
||||
manageUsers: boolean;
|
||||
mentionComment: boolean;
|
||||
// r
|
||||
readComment: boolean;
|
||||
register: boolean;
|
||||
reportMedia: boolean;
|
||||
// s
|
||||
saveMedia: boolean;
|
||||
shareMedia: boolean;
|
||||
// u
|
||||
usersNeedsToBeApproved: boolean;
|
||||
};
|
||||
pages: {
|
||||
home: string | null; // @todo: Check this again
|
||||
about: GlobalMediaCMS['user']['pages']['about'] | null;
|
||||
media: GlobalMediaCMS['user']['pages']['media'] | null;
|
||||
playlists: GlobalMediaCMS['user']['pages']['playlists'] | null;
|
||||
};
|
||||
};
|
||||
|
||||
type MediaCMSConfigMedia = {
|
||||
item: {
|
||||
displayAuthor: boolean;
|
||||
displayViews: boolean;
|
||||
displayPublishDate: boolean;
|
||||
};
|
||||
share: { options: string[] };
|
||||
};
|
||||
|
||||
type MediaCMSConfigNotifications = GlobalMediaCMS['contents']['notifications'];
|
||||
|
||||
type MediaCMSConfigOptions = {
|
||||
pages: {
|
||||
home: GlobalMediaCMS['pages']['home'];
|
||||
search: GlobalMediaCMS['pages']['search'];
|
||||
media: Omit<GlobalMediaCMS['pages']['media'], 'hideViews'> & {
|
||||
displayViews: boolean;
|
||||
};
|
||||
profile: GlobalMediaCMS['pages']['profile'];
|
||||
};
|
||||
embedded: {
|
||||
video: {
|
||||
dimensions: {
|
||||
width: number;
|
||||
widthUnit: 'px';
|
||||
// widthUnit: 'px' | 'percent'; // @note: The unit value "percent" is not used
|
||||
height: number;
|
||||
heightUnit: 'px';
|
||||
// heightUnit: 'px' | 'percent'; // @note: The unit value "percent" is not used
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type MediaCMSConfigPlaylists = GlobalMediaCMS['features']['playlists'];
|
||||
|
||||
type MediaCMSConfigSidebar = GlobalMediaCMS['features']['sideBar'];
|
||||
|
||||
type MediaCMSConfigSite = {
|
||||
api: string;
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
useRoundedCorners: boolean;
|
||||
version: string;
|
||||
};
|
||||
|
||||
type MediaCMSConfigTheme = Pick<GlobalMediaCMS['site'], 'logo'> & GlobalMediaCMS['site']['theme'];
|
||||
|
||||
type MediaCMSConfigUrl = {
|
||||
admin: string; // eg: '/admin'
|
||||
archive: {
|
||||
categories: string; // eg: './categories.html'
|
||||
tags: string; // eg: './tags.html';
|
||||
};
|
||||
changePassword: string; // eg: './change-password.html';
|
||||
embed: string; // eg: 'http://localhost/embed?m=';
|
||||
error404: string; // eg: './error.html';
|
||||
featured: string; // eg: './featured.html';
|
||||
home: string; // eg: './index.html'
|
||||
latest: string; // eg: './latest.html';
|
||||
manage: {
|
||||
comments: string; // eg: './manage-comments.html'
|
||||
media: string; // eg: './manage-media.html';
|
||||
users: string; // eg: './manage-users.html';
|
||||
};
|
||||
members: string; // eg: './members.html';
|
||||
profile: {
|
||||
about: string; // eg: './profile-about.html';
|
||||
media: string; // eg: './profile-media.html';
|
||||
playlists: string; // eg: './profile-playlists.html';
|
||||
shared_by_me: string; // eg: './profile-media.html/shared_by_me';
|
||||
shared_with_me: string; // eg: './profile-media.html/shared_with_me';
|
||||
};
|
||||
recommended: string; // eg: './recommended.html';
|
||||
register: string; // eg: './register.html';
|
||||
search: {
|
||||
base: string; // eg: './search.html';
|
||||
category: string; // eg: './search.html?c=';
|
||||
query: string; // eg: './search.html?q=';
|
||||
tag: string; // eg: './search.html?t=';
|
||||
};
|
||||
signin: string; // eg: './signin.html';
|
||||
signout: string; // eg: './signout.html';
|
||||
user: {
|
||||
addMedia: string; // eg: './add-media.html';
|
||||
editChannel: string; // eg: './edit-channel.html';
|
||||
editProfile: string; // eg: './edit-profile.html';
|
||||
history: string; // eg: './history.html';
|
||||
liked: string; // eg: './liked.html';
|
||||
};
|
||||
};
|
||||
|
||||
export type MediaCMSConfig = {
|
||||
api: MediaCMSConfigApi;
|
||||
contents: MediaCMSConfigContents;
|
||||
enabled: MediaCMSConfigEnabled;
|
||||
member: MediaCMSConfigMember;
|
||||
media: MediaCMSConfigMedia;
|
||||
notifications: MediaCMSConfigNotifications;
|
||||
options: MediaCMSConfigOptions;
|
||||
playlists: MediaCMSConfigPlaylists;
|
||||
sidebar: MediaCMSConfigSidebar;
|
||||
site: MediaCMSConfigSite;
|
||||
theme: MediaCMSConfigTheme;
|
||||
url: MediaCMSConfigUrl;
|
||||
};
|
||||
3
frontend/src/static/js/types/index.ts
Normal file
3
frontend/src/static/js/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './DeepPartial';
|
||||
export * from './GlobalMediaCMS';
|
||||
export * from './MediaCMSConfig';
|
||||
@@ -1,90 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function loadMediaData() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'LOAD_MEDIA_DATA',
|
||||
});
|
||||
}
|
||||
|
||||
export function likeMedia() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'LIKE_MEDIA',
|
||||
});
|
||||
}
|
||||
|
||||
export function dislikeMedia() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'DISLIKE_MEDIA',
|
||||
});
|
||||
}
|
||||
|
||||
export function reportMedia(reportDescription) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REPORT_MEDIA',
|
||||
reportDescription: !!reportDescription ? reportDescription.replace(/\s/g, '') : '',
|
||||
});
|
||||
}
|
||||
|
||||
export function copyShareLink(inputElem) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'COPY_SHARE_LINK',
|
||||
inputElement: inputElem,
|
||||
});
|
||||
}
|
||||
|
||||
export function copyEmbedMediaCode(inputElem) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'COPY_EMBED_MEDIA_CODE',
|
||||
inputElement: inputElem,
|
||||
});
|
||||
}
|
||||
|
||||
export function removeMedia() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REMOVE_MEDIA',
|
||||
});
|
||||
}
|
||||
|
||||
export function submitComment(commentText) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SUBMIT_COMMENT',
|
||||
commentText,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteComment(commentId) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'DELETE_COMMENT',
|
||||
commentId,
|
||||
});
|
||||
}
|
||||
|
||||
export function createPlaylist(playlist_data) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'CREATE_PLAYLIST',
|
||||
playlist_data,
|
||||
});
|
||||
}
|
||||
|
||||
export function addMediaToPlaylist(playlist_id, media_id) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'ADD_MEDIA_TO_PLAYLIST',
|
||||
playlist_id,
|
||||
media_id,
|
||||
});
|
||||
}
|
||||
|
||||
export function removeMediaFromPlaylist(playlist_id, media_id) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REMOVE_MEDIA_FROM_PLAYLIST',
|
||||
playlist_id,
|
||||
media_id,
|
||||
});
|
||||
}
|
||||
|
||||
export function addNewPlaylist(playlist_data) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'APPEND_NEW_PLAYLIST',
|
||||
playlist_data,
|
||||
});
|
||||
}
|
||||
63
frontend/src/static/js/utils/actions/MediaPageActions.ts
Executable file
63
frontend/src/static/js/utils/actions/MediaPageActions.ts
Executable file
@@ -0,0 +1,63 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function loadMediaData() {
|
||||
dispatcher.dispatch({ type: 'LOAD_MEDIA_DATA' });
|
||||
}
|
||||
|
||||
export function likeMedia() {
|
||||
dispatcher.dispatch({ type: 'LIKE_MEDIA' });
|
||||
}
|
||||
|
||||
export function dislikeMedia() {
|
||||
dispatcher.dispatch({ type: 'DISLIKE_MEDIA' });
|
||||
}
|
||||
|
||||
// @todo: Revisit this
|
||||
export function reportMedia(reportDescription?: string | null) {
|
||||
dispatcher.dispatch({
|
||||
type: 'REPORT_MEDIA',
|
||||
reportDescription: typeof reportDescription === 'string' ? reportDescription.replace(/\s/g, '') : '',
|
||||
});
|
||||
}
|
||||
|
||||
export function copyShareLink(inputElem: HTMLInputElement) {
|
||||
dispatcher.dispatch({ type: 'COPY_SHARE_LINK', inputElement: inputElem });
|
||||
}
|
||||
|
||||
export function copyEmbedMediaCode(inputElem: HTMLTextAreaElement) {
|
||||
dispatcher.dispatch({ type: 'COPY_EMBED_MEDIA_CODE', inputElement: inputElem });
|
||||
}
|
||||
|
||||
export function removeMedia() {
|
||||
dispatcher.dispatch({ type: 'REMOVE_MEDIA' });
|
||||
}
|
||||
|
||||
export function submitComment(commentText: string) {
|
||||
dispatcher.dispatch({ type: 'SUBMIT_COMMENT', commentText });
|
||||
}
|
||||
|
||||
export function deleteComment(commentId: string | number) {
|
||||
dispatcher.dispatch({ type: 'DELETE_COMMENT', commentId });
|
||||
}
|
||||
|
||||
export function createPlaylist(playlist_data: { title: string; description: string }) {
|
||||
dispatcher.dispatch({ type: 'CREATE_PLAYLIST', playlist_data });
|
||||
}
|
||||
|
||||
export function addMediaToPlaylist(playlist_id: string, media_id: string) {
|
||||
dispatcher.dispatch({ type: 'ADD_MEDIA_TO_PLAYLIST', playlist_id, media_id });
|
||||
}
|
||||
|
||||
export function removeMediaFromPlaylist(playlist_id: string, media_id: string) {
|
||||
dispatcher.dispatch({ type: 'REMOVE_MEDIA_FROM_PLAYLIST', playlist_id, media_id });
|
||||
}
|
||||
|
||||
export function addNewPlaylist(playlist_data: {
|
||||
playlist_id: string;
|
||||
add_date: Date; // @todo: Revisit this
|
||||
description: string;
|
||||
title: string;
|
||||
media_list: string[]; // @todo: Revisit this
|
||||
}) {
|
||||
dispatcher.dispatch({ type: 'APPEND_NEW_PLAYLIST', playlist_data });
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function initPage(page) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'INIT_PAGE',
|
||||
page,
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleMediaAutoPlay() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'TOGGLE_AUTO_PLAY',
|
||||
});
|
||||
}
|
||||
|
||||
export function addNotification(notification, notificationId) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'ADD_NOTIFICATION',
|
||||
notification,
|
||||
notificationId,
|
||||
});
|
||||
}
|
||||
13
frontend/src/static/js/utils/actions/PageActions.ts
Executable file
13
frontend/src/static/js/utils/actions/PageActions.ts
Executable file
@@ -0,0 +1,13 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function initPage(page: string) {
|
||||
dispatcher.dispatch({ type: 'INIT_PAGE', page });
|
||||
}
|
||||
|
||||
export function toggleMediaAutoPlay() {
|
||||
dispatcher.dispatch({ type: 'TOGGLE_AUTO_PLAY' });
|
||||
}
|
||||
|
||||
export function addNotification(notification: string, notificationId: string) {
|
||||
dispatcher.dispatch({ type: 'ADD_NOTIFICATION', notification, notificationId });
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function loadPlaylistData() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'LOAD_PLAYLIST_DATA',
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleSave() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'TOGGLE_SAVE',
|
||||
});
|
||||
}
|
||||
|
||||
export function updatePlaylist(playlist_data) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'UPDATE_PLAYLIST',
|
||||
playlist_data,
|
||||
});
|
||||
}
|
||||
|
||||
export function removePlaylist() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REMOVE_PLAYLIST',
|
||||
});
|
||||
}
|
||||
|
||||
export function removedMediaFromPlaylist(media_id, playlist_id) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'MEDIA_REMOVED_FROM_PLAYLIST',
|
||||
media_id,
|
||||
playlist_id,
|
||||
});
|
||||
}
|
||||
|
||||
export function reorderedMediaInPlaylist(newMediaData) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'PLAYLIST_MEDIA_REORDERED',
|
||||
playlist_media: newMediaData,
|
||||
});
|
||||
}
|
||||
26
frontend/src/static/js/utils/actions/PlaylistPageActions.ts
Executable file
26
frontend/src/static/js/utils/actions/PlaylistPageActions.ts
Executable file
@@ -0,0 +1,26 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function loadPlaylistData() {
|
||||
dispatcher.dispatch({ type: 'LOAD_PLAYLIST_DATA' });
|
||||
}
|
||||
|
||||
export function toggleSave() {
|
||||
dispatcher.dispatch({ type: 'TOGGLE_SAVE' });
|
||||
}
|
||||
|
||||
export function updatePlaylist(playlist_data: { title: string; description: string }) {
|
||||
dispatcher.dispatch({ type: 'UPDATE_PLAYLIST', playlist_data });
|
||||
}
|
||||
|
||||
export function removePlaylist() {
|
||||
dispatcher.dispatch({ type: 'REMOVE_PLAYLIST' });
|
||||
}
|
||||
|
||||
export function removedMediaFromPlaylist(media_id: string, playlist_id: string) {
|
||||
dispatcher.dispatch({ type: 'MEDIA_REMOVED_FROM_PLAYLIST', media_id, playlist_id });
|
||||
}
|
||||
|
||||
// @todo: Revisit this
|
||||
export function reorderedMediaInPlaylist(newMediaData: { [k: string]: any; thumbnail_url: string; url: string }[]) {
|
||||
dispatcher.dispatch({ type: 'PLAYLIST_MEDIA_REORDERED', playlist_media: newMediaData });
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function toggleLoop() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'TOGGLE_LOOP',
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleShuffle() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'TOGGLE_SHUFFLE',
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleSave() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'TOGGLE_SAVE',
|
||||
});
|
||||
}
|
||||
13
frontend/src/static/js/utils/actions/PlaylistViewActions.ts
Executable file
13
frontend/src/static/js/utils/actions/PlaylistViewActions.ts
Executable file
@@ -0,0 +1,13 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function toggleLoop() {
|
||||
dispatcher.dispatch({ type: 'TOGGLE_LOOP' });
|
||||
}
|
||||
|
||||
export function toggleShuffle() {
|
||||
dispatcher.dispatch({ type: 'TOGGLE_SHUFFLE' });
|
||||
}
|
||||
|
||||
export function toggleSave() {
|
||||
dispatcher.dispatch({ type: 'TOGGLE_SAVE' });
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function load_author_data() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'LOAD_AUTHOR_DATA',
|
||||
});
|
||||
}
|
||||
|
||||
export function remove_profile() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REMOVE_PROFILE',
|
||||
});
|
||||
}
|
||||
9
frontend/src/static/js/utils/actions/ProfilePageActions.ts
Executable file
9
frontend/src/static/js/utils/actions/ProfilePageActions.ts
Executable file
@@ -0,0 +1,9 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function load_author_data() {
|
||||
dispatcher.dispatch({ type: 'LOAD_AUTHOR_DATA' });
|
||||
}
|
||||
|
||||
export function remove_profile() {
|
||||
dispatcher.dispatch({ type: 'REMOVE_PROFILE' });
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function requestPredictions(query) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REQUEST_PREDICTIONS',
|
||||
query,
|
||||
});
|
||||
}
|
||||
5
frontend/src/static/js/utils/actions/SearchFieldActions.ts
Executable file
5
frontend/src/static/js/utils/actions/SearchFieldActions.ts
Executable file
@@ -0,0 +1,5 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function requestPredictions(query: string) {
|
||||
dispatcher.dispatch({ type: 'REQUEST_PREDICTIONS', query });
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function set_viewer_mode(inTheaterMode) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SET_VIEWER_MODE',
|
||||
inTheaterMode,
|
||||
});
|
||||
}
|
||||
|
||||
export function set_player_volume(playerVolume) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SET_PLAYER_VOLUME',
|
||||
playerVolume,
|
||||
});
|
||||
}
|
||||
|
||||
export function set_player_sound_muted(playerSoundMuted) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SET_PLAYER_SOUND_MUTED',
|
||||
playerSoundMuted,
|
||||
});
|
||||
}
|
||||
|
||||
export function set_video_quality(quality) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SET_VIDEO_QUALITY',
|
||||
quality,
|
||||
});
|
||||
}
|
||||
|
||||
export function set_video_playback_speed(playbackSpeed) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SET_VIDEO_PLAYBACK_SPEED',
|
||||
playbackSpeed,
|
||||
});
|
||||
}
|
||||
23
frontend/src/static/js/utils/actions/VideoViewerActions.ts
Executable file
23
frontend/src/static/js/utils/actions/VideoViewerActions.ts
Executable file
@@ -0,0 +1,23 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function set_viewer_mode(inTheaterMode: boolean) {
|
||||
dispatcher.dispatch({ type: 'SET_VIEWER_MODE', inTheaterMode });
|
||||
}
|
||||
|
||||
export function set_player_volume(playerVolume: number) {
|
||||
dispatcher.dispatch({ type: 'SET_PLAYER_VOLUME', playerVolume });
|
||||
}
|
||||
|
||||
export function set_player_sound_muted(playerSoundMuted: boolean) {
|
||||
dispatcher.dispatch({ type: 'SET_PLAYER_SOUND_MUTED', playerSoundMuted });
|
||||
}
|
||||
|
||||
export function set_video_quality(
|
||||
quality: 'auto' | number // @todo: Check this again
|
||||
) {
|
||||
dispatcher.dispatch({ type: 'SET_VIDEO_QUALITY', quality });
|
||||
}
|
||||
|
||||
export function set_video_playback_speed(playbackSpeed: number) {
|
||||
dispatcher.dispatch({ type: 'SET_VIDEO_PLAYBACK_SPEED', playbackSpeed });
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as months } from './months';
|
||||
export { default as weekdays } from './weekdays';
|
||||
2
frontend/src/static/js/utils/constants/index.ts
Normal file
2
frontend/src/static/js/utils/constants/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './months';
|
||||
export * from './weekdays';
|
||||
@@ -1,14 +0,0 @@
|
||||
export default [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
14
frontend/src/static/js/utils/constants/months.ts
Executable file
14
frontend/src/static/js/utils/constants/months.ts
Executable file
@@ -0,0 +1,14 @@
|
||||
export const months = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
] as const;
|
||||
@@ -1 +0,0 @@
|
||||
export default ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
1
frontend/src/static/js/utils/constants/weekdays.ts
Executable file
1
frontend/src/static/js/utils/constants/weekdays.ts
Executable file
@@ -0,0 +1 @@
|
||||
export const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] as const;
|
||||
@@ -1,5 +0,0 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
|
||||
export const ApiUrlContext = createContext(mediacmsConfig(window.MediaCMS).api);
|
||||
export const ApiUrlConsumer = ApiUrlContext.Consumer;
|
||||
5
frontend/src/static/js/utils/contexts/ApiUrlContext.ts
Normal file
5
frontend/src/static/js/utils/contexts/ApiUrlContext.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const ApiUrlContext = createContext(mediacmsConfig(window.MediaCMS).api);
|
||||
export const ApiUrlConsumer = ApiUrlContext.Consumer;
|
||||
@@ -1,130 +0,0 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
import { translateString } from '../../utils/helpers/';
|
||||
|
||||
const config = mediacmsConfig(window.MediaCMS);
|
||||
|
||||
const links = config.url;
|
||||
const theme = config.theme;
|
||||
const user = config.member;
|
||||
|
||||
const hasThemeSwitcher = theme.switch.enabled && 'header' === theme.switch.position;
|
||||
|
||||
function popupTopNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (!user.is.anonymous) {
|
||||
if (user.can.addMedia) {
|
||||
items.push({
|
||||
link: links.user.addMedia,
|
||||
icon: 'video_call',
|
||||
text: translateString('Upload media'),
|
||||
itemAttr: {
|
||||
className: 'visible-only-in-small',
|
||||
},
|
||||
});
|
||||
|
||||
if (user.pages.media) {
|
||||
items.push({
|
||||
link: user.pages.media,
|
||||
icon: 'video_library',
|
||||
text: translateString('My media'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
link: links.signout,
|
||||
icon: 'exit_to_app',
|
||||
text: translateString('Sign out'),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function popupMiddleNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (hasThemeSwitcher) {
|
||||
items.push({
|
||||
itemType: 'open-subpage',
|
||||
icon: 'brightness_4',
|
||||
iconPos: 'left',
|
||||
text: 'Switch theme',
|
||||
buttonAttr: {
|
||||
className: 'change-page',
|
||||
'data-page-id': 'switch-theme',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (user.is.anonymous) {
|
||||
if (user.can.login) {
|
||||
items.push({
|
||||
itemType: 'link',
|
||||
icon: 'login',
|
||||
iconPos: 'left',
|
||||
text: translateString('Sign in'),
|
||||
link: links.signin,
|
||||
linkAttr: {
|
||||
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (user.can.register) {
|
||||
items.push({
|
||||
itemType: 'link',
|
||||
icon: 'person_add',
|
||||
iconPos: 'left',
|
||||
text: translateString('Register'),
|
||||
link: links.register,
|
||||
linkAttr: {
|
||||
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push({
|
||||
link: links.user.editProfile,
|
||||
icon: 'brush',
|
||||
text: translateString('Edit profile'),
|
||||
});
|
||||
|
||||
if (user.can.changePassword) {
|
||||
items.push({
|
||||
link: links.changePassword,
|
||||
icon: 'lock',
|
||||
text: translateString('Change password'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function popupBottomNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (user.is.admin) {
|
||||
items.push({
|
||||
link: links.admin,
|
||||
icon: 'admin_panel_settings',
|
||||
text: 'MediaCMS administration',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export const HeaderContext = createContext({
|
||||
hasThemeSwitcher,
|
||||
popupNavItems: {
|
||||
top: popupTopNavItems(),
|
||||
middle: popupMiddleNavItems(),
|
||||
bottom: popupBottomNavItems(),
|
||||
},
|
||||
});
|
||||
|
||||
export const HeaderConsumer = HeaderContext.Consumer;
|
||||
130
frontend/src/static/js/utils/contexts/HeaderContext.ts
Normal file
130
frontend/src/static/js/utils/contexts/HeaderContext.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
import { translateString } from '../helpers';
|
||||
|
||||
const config = mediacmsConfig(window.MediaCMS);
|
||||
|
||||
const links = config.url;
|
||||
const theme = config.theme;
|
||||
const user = config.member;
|
||||
|
||||
const hasThemeSwitcher = theme.switch.enabled && 'header' === theme.switch.position;
|
||||
|
||||
function popupTopNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (!user.is.anonymous) {
|
||||
if (user.can.addMedia) {
|
||||
items.push({
|
||||
link: links.user.addMedia,
|
||||
icon: 'video_call',
|
||||
text: translateString('Upload media'),
|
||||
itemAttr: {
|
||||
className: 'visible-only-in-small',
|
||||
},
|
||||
});
|
||||
|
||||
if (user.pages.media) {
|
||||
items.push({
|
||||
link: user.pages.media,
|
||||
icon: 'video_library',
|
||||
text: translateString('My media'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
link: links.signout,
|
||||
icon: 'exit_to_app',
|
||||
text: translateString('Sign out'),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function popupMiddleNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (hasThemeSwitcher) {
|
||||
items.push({
|
||||
itemType: 'open-subpage',
|
||||
icon: 'brightness_4',
|
||||
iconPos: 'left',
|
||||
text: 'Switch theme',
|
||||
buttonAttr: {
|
||||
className: 'change-page',
|
||||
'data-page-id': 'switch-theme',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (user.is.anonymous) {
|
||||
if (user.can.login) {
|
||||
items.push({
|
||||
itemType: 'link',
|
||||
icon: 'login',
|
||||
iconPos: 'left',
|
||||
text: translateString('Sign in'),
|
||||
link: links.signin,
|
||||
linkAttr: {
|
||||
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (user.can.register) {
|
||||
items.push({
|
||||
itemType: 'link',
|
||||
icon: 'person_add',
|
||||
iconPos: 'left',
|
||||
text: translateString('Register'),
|
||||
link: links.register,
|
||||
linkAttr: {
|
||||
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push({
|
||||
link: links.user.editProfile,
|
||||
icon: 'brush',
|
||||
text: translateString('Edit profile'),
|
||||
});
|
||||
|
||||
if (user.can.changePassword) {
|
||||
items.push({
|
||||
link: links.changePassword,
|
||||
icon: 'lock',
|
||||
text: translateString('Change password'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function popupBottomNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (user.is.admin) {
|
||||
items.push({
|
||||
link: links.admin,
|
||||
icon: 'admin_panel_settings',
|
||||
text: 'MediaCMS administration',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export const HeaderContext = createContext({
|
||||
hasThemeSwitcher,
|
||||
popupNavItems: {
|
||||
top: popupTopNavItems(),
|
||||
middle: popupMiddleNavItems(),
|
||||
bottom: popupBottomNavItems(),
|
||||
},
|
||||
});
|
||||
|
||||
export const HeaderConsumer = HeaderContext.Consumer;
|
||||
@@ -1,13 +1,15 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { BrowserCache } from '../classes/';
|
||||
import { PageStore } from '../stores/';
|
||||
import { addClassname, removeClassname, inEmbeddedApp } from '../helpers/';
|
||||
import React, { createContext, ReactNode, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { BrowserCache } from '../classes';
|
||||
import { PageStore } from '../stores';
|
||||
import { addClassname, removeClassname, inEmbeddedApp } from '../helpers';
|
||||
import SiteContext from './SiteContext';
|
||||
|
||||
let slidingSidebarTimeout;
|
||||
let slidingSidebarTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
function onSidebarVisibilityChange(visibleSidebar) {
|
||||
clearTimeout(slidingSidebarTimeout);
|
||||
function onSidebarVisibilityChange(visibleSidebar: boolean) {
|
||||
if (slidingSidebarTimeout) {
|
||||
clearTimeout(slidingSidebarTimeout);
|
||||
}
|
||||
|
||||
addClassname(document.body, 'sliding-sidebar');
|
||||
|
||||
@@ -39,18 +41,29 @@ function onSidebarVisibilityChange(visibleSidebar) {
|
||||
}, 20);
|
||||
}
|
||||
|
||||
export const LayoutContext = createContext();
|
||||
export const LayoutContext = createContext({
|
||||
enabledSidebar: true,
|
||||
visibleSidebar: true,
|
||||
setVisibleSidebar: (_: boolean) => {},
|
||||
visibleMobileSearch: false,
|
||||
toggleMobileSearch: () => {},
|
||||
toggleSidebar: () => {},
|
||||
});
|
||||
|
||||
export const LayoutProvider = ({ children }) => {
|
||||
export const LayoutProvider = ({ children }: { children: ReactNode }) => {
|
||||
const site = useContext(SiteContext);
|
||||
const cache = new BrowserCache('MediaCMS[' + site.id + '][layout]', 86400);
|
||||
const cache = BrowserCache('MediaCMS[' + site.id + '][layout]', 86400);
|
||||
|
||||
const isMediaPage = useMemo(() => PageStore.get('current-page') === 'media', []);
|
||||
const isEmbeddedApp = useMemo(() => inEmbeddedApp(), []);
|
||||
|
||||
const enabledSidebar = Boolean(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar'));
|
||||
|
||||
const [visibleSidebar, setVisibleSidebar] = useState(cache.get('visible-sidebar'));
|
||||
const [visibleSidebar, setVisibleSidebar] = useState<boolean>(
|
||||
cache instanceof Error
|
||||
? true // @todo: Check this again
|
||||
: cache.get('visible-sidebar')
|
||||
);
|
||||
const [visibleMobileSearch, setVisibleMobileSearch] = useState(false);
|
||||
|
||||
const toggleMobileSearch = () => {
|
||||
@@ -71,7 +84,9 @@ export const LayoutProvider = ({ children }) => {
|
||||
}
|
||||
|
||||
if (!isEmbeddedApp && !isMediaPage && 1023 < window.innerWidth) {
|
||||
cache.set('visible-sidebar', visibleSidebar);
|
||||
if (!(cache instanceof Error)) {
|
||||
cache.set('visible-sidebar', visibleSidebar);
|
||||
}
|
||||
}
|
||||
}, [isEmbeddedApp, isMediaPage, visibleSidebar]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const LinksContext = createContext(mediacmsConfig(window.MediaCMS).url);
|
||||
export const LinksConsumer = LinksContext.Consumer;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const MemberContext = createContext(mediacmsConfig(window.MediaCMS).member);
|
||||
export const MemberConsumer = MemberContext.Consumer;
|
||||
@@ -1,4 +0,0 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
|
||||
export const PlaylistsContext = createContext(mediacmsConfig(window.MediaCMS).playlists);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const PlaylistsContext = createContext(mediacmsConfig(window.MediaCMS).playlists);
|
||||
@@ -1,5 +0,0 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
|
||||
export const ShareOptionsContext = createContext(mediacmsConfig(window.MediaCMS).media.share.options);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const ShareOptionsContext = createContext(mediacmsConfig(window.MediaCMS).media.share.options);
|
||||
@@ -1,5 +0,0 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
|
||||
export const SidebarContext = createContext(mediacmsConfig(window.MediaCMS).sidebar);
|
||||
export const SidebarConsumer = SidebarContext.Consumer;
|
||||
5
frontend/src/static/js/utils/contexts/SidebarContext.ts
Normal file
5
frontend/src/static/js/utils/contexts/SidebarContext.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const SidebarContext = createContext(mediacmsConfig(window.MediaCMS).sidebar);
|
||||
export const SidebarConsumer = SidebarContext.Consumer;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const SiteContext = createContext(mediacmsConfig(window.MediaCMS).site);
|
||||
export const SiteConsumer = SiteContext.Consumer;
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
const notifications = mediacmsConfig(window.MediaCMS).notifications.messages;
|
||||
|
||||
const texts = {
|
||||
notifications,
|
||||
};
|
||||
const texts = { notifications };
|
||||
|
||||
export const TextsContext = createContext(texts);
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { BrowserCache } from '../classes/';
|
||||
import { addClassname, removeClassname, supportsSvgAsImg } from '../helpers/';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
import SiteContext from './SiteContext';
|
||||
|
||||
const config = mediacmsConfig(window.MediaCMS);
|
||||
|
||||
function initLogo(logo) {
|
||||
let light = null;
|
||||
let dark = null;
|
||||
|
||||
if (void 0 !== logo.darkMode) {
|
||||
if (supportsSvgAsImg() && void 0 !== logo.darkMode.svg && '' !== logo.darkMode.svg) {
|
||||
dark = logo.darkMode.svg;
|
||||
} else if (void 0 !== logo.darkMode.img && '' !== logo.darkMode.img) {
|
||||
dark = logo.darkMode.img;
|
||||
}
|
||||
}
|
||||
|
||||
if (void 0 !== logo.lightMode) {
|
||||
if (supportsSvgAsImg() && void 0 !== logo.lightMode.svg && '' !== logo.lightMode.svg) {
|
||||
light = logo.lightMode.svg;
|
||||
} else if (void 0 !== logo.lightMode.img && '' !== logo.lightMode.img) {
|
||||
light = logo.lightMode.img;
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== light || null !== dark) {
|
||||
if (null === light) {
|
||||
light = dark;
|
||||
} else if (null === dark) {
|
||||
dark = light;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
light,
|
||||
dark,
|
||||
};
|
||||
}
|
||||
|
||||
function initMode(cachedValue, defaultValue) {
|
||||
return 'light' === cachedValue || 'dark' === cachedValue ? cachedValue : defaultValue;
|
||||
}
|
||||
|
||||
export const ThemeContext = createContext();
|
||||
|
||||
export const ThemeProvider = ({ children }) => {
|
||||
const site = useContext(SiteContext);
|
||||
const cache = new BrowserCache('MediaCMS[' + site.id + '][theme]', 86400);
|
||||
const [themeMode, setThemeMode] = useState(initMode(cache.get('mode'), config.theme.mode));
|
||||
const logos = initLogo(config.theme.logo);
|
||||
const [logo, setLogo] = useState(logos[themeMode]);
|
||||
|
||||
const changeMode = () => {
|
||||
setThemeMode('light' === themeMode ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if ('dark' === themeMode) {
|
||||
addClassname(document.body, 'dark_theme');
|
||||
} else {
|
||||
removeClassname(document.body, 'dark_theme');
|
||||
}
|
||||
cache.set('mode', themeMode);
|
||||
setLogo(logos[themeMode]);
|
||||
}, [themeMode]);
|
||||
|
||||
const value = {
|
||||
logo,
|
||||
currentThemeMode: themeMode,
|
||||
changeThemeMode: changeMode,
|
||||
themeModeSwitcher: config.theme.switch,
|
||||
};
|
||||
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
||||
};
|
||||
|
||||
export const ThemeConsumer = ThemeContext.Consumer;
|
||||
95
frontend/src/static/js/utils/contexts/ThemeContext.tsx
Normal file
95
frontend/src/static/js/utils/contexts/ThemeContext.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import { GlobalMediaCMS } from '../../types';
|
||||
import { BrowserCache } from '../classes';
|
||||
import { addClassname, removeClassname, supportsSvgAsImg } from '../helpers';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
import SiteContext from './SiteContext';
|
||||
|
||||
const config = mediacmsConfig(window.MediaCMS);
|
||||
|
||||
function initLogo(logo: GlobalMediaCMS['site']['logo']) {
|
||||
let light = null;
|
||||
let dark = null;
|
||||
|
||||
if (void 0 !== logo.darkMode) {
|
||||
if (supportsSvgAsImg() && void 0 !== logo.darkMode.svg && '' !== logo.darkMode.svg) {
|
||||
dark = logo.darkMode.svg;
|
||||
} else if (void 0 !== logo.darkMode.img && '' !== logo.darkMode.img) {
|
||||
dark = logo.darkMode.img;
|
||||
}
|
||||
}
|
||||
|
||||
if (void 0 !== logo.lightMode) {
|
||||
if (supportsSvgAsImg() && void 0 !== logo.lightMode.svg && '' !== logo.lightMode.svg) {
|
||||
light = logo.lightMode.svg;
|
||||
} else if (void 0 !== logo.lightMode.img && '' !== logo.lightMode.img) {
|
||||
light = logo.lightMode.img;
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== light || null !== dark) {
|
||||
if (null === light) {
|
||||
light = dark;
|
||||
} else if (null === dark) {
|
||||
dark = light;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
light,
|
||||
dark,
|
||||
};
|
||||
}
|
||||
|
||||
function initMode(cachedValue: string | undefined, defaultValue: GlobalMediaCMS['site']['theme']['mode']) {
|
||||
return 'light' === cachedValue || 'dark' === cachedValue ? cachedValue : defaultValue;
|
||||
}
|
||||
|
||||
export const ThemeContext = createContext({
|
||||
logo: initLogo(config.theme.logo)[config.theme.mode],
|
||||
currentThemeMode: config.theme.mode,
|
||||
changeThemeMode: () => {},
|
||||
themeModeSwitcher: config.theme.switch,
|
||||
});
|
||||
|
||||
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||
const site = useContext(SiteContext);
|
||||
|
||||
const cache = BrowserCache('MediaCMS[' + site.id + '][theme]', 86400);
|
||||
|
||||
const [themeMode, setThemeMode] = useState(
|
||||
initMode(cache instanceof Error ? undefined : cache.get('mode'), config.theme.mode)
|
||||
);
|
||||
|
||||
const logos = initLogo(config.theme.logo);
|
||||
const [logo, setLogo] = useState(logos[themeMode]);
|
||||
|
||||
const changeMode = () => {
|
||||
setThemeMode('light' === themeMode ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if ('dark' === themeMode) {
|
||||
addClassname(document.body, 'dark_theme');
|
||||
} else {
|
||||
removeClassname(document.body, 'dark_theme');
|
||||
}
|
||||
|
||||
if (!(cache instanceof Error)) {
|
||||
cache.set('mode', themeMode);
|
||||
}
|
||||
|
||||
setLogo(logos[themeMode]);
|
||||
}, [themeMode]);
|
||||
|
||||
const value = {
|
||||
logo,
|
||||
currentThemeMode: themeMode,
|
||||
changeThemeMode: changeMode,
|
||||
themeModeSwitcher: config.theme.switch,
|
||||
};
|
||||
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
||||
};
|
||||
|
||||
export const ThemeConsumer = ThemeContext.Consumer;
|
||||
@@ -1,22 +0,0 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
|
||||
export const UserContext = createContext();
|
||||
|
||||
const member = mediacmsConfig(window.MediaCMS).member;
|
||||
|
||||
export const UserProvider = ({ children }) => {
|
||||
const value = {
|
||||
isAnonymous: member.is.anonymous,
|
||||
username: member.username,
|
||||
thumbnail: member.thumbnail,
|
||||
userCan: member.can,
|
||||
pages: member.pages,
|
||||
};
|
||||
|
||||
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
||||
};
|
||||
|
||||
export const UserConsumer = UserContext.Consumer;
|
||||
|
||||
export default UserContext;
|
||||
28
frontend/src/static/js/utils/contexts/UserContext.tsx
Normal file
28
frontend/src/static/js/utils/contexts/UserContext.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { createContext, ReactNode } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
const member = mediacmsConfig(window.MediaCMS).member;
|
||||
export const UserContext = createContext({
|
||||
isAnonymous: member.is.anonymous,
|
||||
username: member.username,
|
||||
thumbnail: member.thumbnail,
|
||||
userCan: member.can,
|
||||
pages: member.pages,
|
||||
});
|
||||
|
||||
export function UserProvider({ children }: { children: ReactNode }) {
|
||||
const value = {
|
||||
isAnonymous: member.is.anonymous,
|
||||
username: member.username,
|
||||
thumbnail: member.thumbnail,
|
||||
userCan: member.can,
|
||||
pages: member.pages,
|
||||
};
|
||||
|
||||
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
||||
}
|
||||
|
||||
export const UserConsumer = UserContext.Consumer;
|
||||
|
||||
export default UserContext;
|
||||
@@ -1,2 +0,0 @@
|
||||
const Dispatcher = require('flux').Dispatcher;
|
||||
module.exports = new Dispatcher();
|
||||
3
frontend/src/static/js/utils/dispatcher.ts
Executable file
3
frontend/src/static/js/utils/dispatcher.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
import { Dispatcher } from 'flux';
|
||||
|
||||
export const dispatcher = new Dispatcher();
|
||||
@@ -1,19 +0,0 @@
|
||||
export function csrfToken() {
|
||||
var i,
|
||||
cookies,
|
||||
cookie,
|
||||
cookieVal = null;
|
||||
if (document.cookie && '' !== document.cookie) {
|
||||
cookies = document.cookie.split(';');
|
||||
i = 0;
|
||||
while (i < cookies.length) {
|
||||
cookie = cookies[i].trim();
|
||||
if ('csrftoken=' === cookie.substring(0, 10)) {
|
||||
cookieVal = decodeURIComponent(cookie.substring(10));
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return cookieVal;
|
||||
}
|
||||
18
frontend/src/static/js/utils/helpers/csrfToken.ts
Executable file
18
frontend/src/static/js/utils/helpers/csrfToken.ts
Executable file
@@ -0,0 +1,18 @@
|
||||
export function csrfToken() {
|
||||
let cookieVal = null;
|
||||
|
||||
if (document.cookie && '' !== document.cookie) {
|
||||
const cookies = document.cookie.split(';');
|
||||
let i = 0;
|
||||
while (i < cookies.length) {
|
||||
const cookie = cookies[i].trim();
|
||||
if ('csrftoken=' === cookie.substring(0, 10)) {
|
||||
cookieVal = decodeURIComponent(cookie.substring(10));
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return cookieVal;
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
export function supportsSvgAsImg() {
|
||||
// @link: https://github.com/Modernizr/Modernizr/blob/master/feature-detects/svg/asimg.js
|
||||
return document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#Image', '1.1');
|
||||
}
|
||||
|
||||
export function removeClassname(el, cls) {
|
||||
if (el.classList) {
|
||||
el.classList.remove(cls);
|
||||
} else {
|
||||
el.className = el.className.replace(new RegExp('(^|\\b)' + cls.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
||||
}
|
||||
}
|
||||
|
||||
export function addClassname(el, cls) {
|
||||
if (el.classList) {
|
||||
el.classList.add(cls);
|
||||
} else {
|
||||
el.className += ' ' + cls;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasClassname(el, cls) {
|
||||
return el.className && new RegExp('(\\s|^)' + cls + '(\\s|$)').test(el.className);
|
||||
}
|
||||
|
||||
export const cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
|
||||
|
||||
export const requestAnimationFrame =
|
||||
window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
|
||||
|
||||
export function BrowserEvents() {
|
||||
const callbacks = {
|
||||
document: {
|
||||
visibility: [],
|
||||
},
|
||||
window: {
|
||||
resize: [],
|
||||
scroll: [],
|
||||
},
|
||||
};
|
||||
|
||||
function onDocumentVisibilityChange() {
|
||||
callbacks.document.visibility.map((fn) => fn());
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
callbacks.window.resize.map((fn) => fn());
|
||||
}
|
||||
|
||||
function onWindowScroll() {
|
||||
callbacks.window.scroll.map((fn) => fn());
|
||||
}
|
||||
|
||||
function windowEvents(resizeCallback, scrollCallback) {
|
||||
if ('function' === typeof resizeCallback) {
|
||||
callbacks.window.resize.push(resizeCallback);
|
||||
}
|
||||
|
||||
if ('function' === typeof scrollCallback) {
|
||||
callbacks.window.scroll.push(scrollCallback);
|
||||
}
|
||||
}
|
||||
|
||||
function documentEvents(visibilityChangeCallback) {
|
||||
if ('function' === typeof visibilityChangeCallback) {
|
||||
callbacks.document.visibility.push(visibilityChangeCallback);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', onDocumentVisibilityChange);
|
||||
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
window.addEventListener('scroll', onWindowScroll);
|
||||
|
||||
return {
|
||||
doc: documentEvents,
|
||||
win: windowEvents,
|
||||
};
|
||||
}
|
||||
95
frontend/src/static/js/utils/helpers/dom.ts
Executable file
95
frontend/src/static/js/utils/helpers/dom.ts
Executable file
@@ -0,0 +1,95 @@
|
||||
export function supportsSvgAsImg() {
|
||||
// @link: https://github.com/Modernizr/Modernizr/blob/master/feature-detects/svg/asimg.js
|
||||
return document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#Image', '1.1');
|
||||
}
|
||||
|
||||
export function removeClassname(el: HTMLElement, cls: string) {
|
||||
if (el.classList) {
|
||||
el.classList.remove(cls);
|
||||
} else {
|
||||
el.className = el.className.replace(new RegExp('(^|\\b)' + cls.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
||||
}
|
||||
}
|
||||
|
||||
export function addClassname(el: HTMLElement, cls: string) {
|
||||
if (el.classList) {
|
||||
el.classList.add(cls);
|
||||
} else {
|
||||
el.className += ' ' + cls;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasClassname(el: HTMLElement, cls: string) {
|
||||
return el.className && new RegExp('(\\s|^)' + cls + '(\\s|$)').test(el.className);
|
||||
}
|
||||
|
||||
type LegacyWindow = Window & {
|
||||
mozCancelAnimationFrame?: Window['cancelAnimationFrame'];
|
||||
mozRequestAnimationFrame?: Window['requestAnimationFrame'];
|
||||
msRequestAnimationFrame?: Window['requestAnimationFrame'];
|
||||
webkitRequestAnimationFrame?: Window['requestAnimationFrame'];
|
||||
};
|
||||
|
||||
const legacyWindow = window as LegacyWindow;
|
||||
|
||||
export const cancelAnimationFrame: Window['cancelAnimationFrame'] =
|
||||
legacyWindow.cancelAnimationFrame ||
|
||||
legacyWindow.mozCancelAnimationFrame ||
|
||||
((id: number) => window.clearTimeout(id));
|
||||
|
||||
export const requestAnimationFrame: Window['requestAnimationFrame'] =
|
||||
legacyWindow.requestAnimationFrame ||
|
||||
legacyWindow.mozRequestAnimationFrame ||
|
||||
legacyWindow.webkitRequestAnimationFrame ||
|
||||
legacyWindow.msRequestAnimationFrame ||
|
||||
((callback: FrameRequestCallback) => window.setTimeout(() => callback(performance.now()), 16));
|
||||
|
||||
export function BrowserEvents() {
|
||||
const callbacks = {
|
||||
document: {
|
||||
visibility: [] as Function[],
|
||||
},
|
||||
window: {
|
||||
resize: [] as Function[],
|
||||
scroll: [] as Function[],
|
||||
},
|
||||
};
|
||||
|
||||
function onDocumentVisibilityChange() {
|
||||
callbacks.document.visibility.map((fn) => fn());
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
callbacks.window.resize.map((fn) => fn());
|
||||
}
|
||||
|
||||
function onWindowScroll() {
|
||||
callbacks.window.scroll.map((fn) => fn());
|
||||
}
|
||||
|
||||
function windowEvents(resizeCallback?: Function, scrollCallback?: Function) {
|
||||
if ('function' === typeof resizeCallback) {
|
||||
callbacks.window.resize.push(resizeCallback);
|
||||
}
|
||||
|
||||
if ('function' === typeof scrollCallback) {
|
||||
callbacks.window.scroll.push(scrollCallback);
|
||||
}
|
||||
}
|
||||
|
||||
function documentEvents(visibilityChangeCallback?: Function) {
|
||||
if ('function' === typeof visibilityChangeCallback) {
|
||||
callbacks.document.visibility.push(visibilityChangeCallback);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', onDocumentVisibilityChange);
|
||||
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
window.addEventListener('scroll', onWindowScroll);
|
||||
|
||||
return {
|
||||
doc: documentEvents,
|
||||
win: windowEvents,
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export function inEmbeddedApp() {
|
||||
sessionStorage.setItem('media_cms_embed_mode', 'true');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (mode === 'standard') {
|
||||
sessionStorage.removeItem('media_cms_embed_mode');
|
||||
return false;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// TODO: Improve or (even better) remove this file code.
|
||||
|
||||
import { error as logErrFn, warn as logWarnFn } from './log';
|
||||
|
||||
function logAndReturnError(logFn, msgArr, ErrorConstructor) {
|
||||
let err;
|
||||
switch (ErrorConstructor) {
|
||||
case TypeError:
|
||||
case RangeError:
|
||||
case SyntaxError:
|
||||
case ReferenceError:
|
||||
err = new ErrorConstructor(msgArr[0]);
|
||||
break;
|
||||
default:
|
||||
err = new Error(msgArr[0]);
|
||||
}
|
||||
logFn(err.message, ...msgArr.slice(1));
|
||||
return err;
|
||||
}
|
||||
|
||||
export function logErrorAndReturnError(msgArr, ErrorConstructor) {
|
||||
return logAndReturnError(logErrFn, msgArr, ErrorConstructor);
|
||||
}
|
||||
|
||||
export function logWarningAndReturnError(msgArr, ErrorConstructor) {
|
||||
return logAndReturnError(logWarnFn, msgArr, ErrorConstructor);
|
||||
}
|
||||
15
frontend/src/static/js/utils/helpers/errors.ts
Executable file
15
frontend/src/static/js/utils/helpers/errors.ts
Executable file
@@ -0,0 +1,15 @@
|
||||
// @todo: Improve or (even better) remove this file.
|
||||
|
||||
import { error, warn } from './log';
|
||||
|
||||
export function logErrorAndReturnError(msgArr: string[]) {
|
||||
const err = new Error(msgArr[0]);
|
||||
error(...msgArr);
|
||||
return err;
|
||||
}
|
||||
|
||||
export function logWarningAndReturnError(msgArr: string[]) {
|
||||
const err = new Error(msgArr[0]);
|
||||
warn(...msgArr);
|
||||
return err;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import * as dispatcher from '../dispatcher.js';
|
||||
export default function (store, handler) {
|
||||
dispatcher.register(store[handler].bind(store));
|
||||
return store;
|
||||
}
|
||||
28
frontend/src/static/js/utils/helpers/exportStore.ts
Executable file
28
frontend/src/static/js/utils/helpers/exportStore.ts
Executable file
@@ -0,0 +1,28 @@
|
||||
import EventEmitter from 'events';
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
// type ClassProperties<C> = {
|
||||
// [Key in keyof C as C[Key] extends Function ? never : Key]: C[Key];
|
||||
// };
|
||||
|
||||
type ClassMethods<C> = {
|
||||
[Key in keyof C as C[Key] extends Function ? Key : never]: C[Key];
|
||||
};
|
||||
|
||||
// @todo: Check this again
|
||||
export function exportStore<TStore extends EventEmitter, THandler extends keyof ClassMethods<TStore>>(
|
||||
store: TStore,
|
||||
handler: THandler
|
||||
) {
|
||||
const method = store[handler] as Function;
|
||||
const callback: (payload: unknown) => void = method.bind(store);
|
||||
dispatcher.register(callback);
|
||||
return store;
|
||||
}
|
||||
|
||||
// @todo: Remove older vesion.
|
||||
// export function exportStore_OLD(store, handler) {
|
||||
// const callback = store[handler].bind(store);
|
||||
// dispatcher.register(callback);
|
||||
// return store;
|
||||
// }
|
||||
@@ -1,11 +0,0 @@
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
export function formatInnerLink(url, baseUrl) {
|
||||
let link = urlParse(url, {});
|
||||
|
||||
if ('' === link.origin || 'null' === link.origin || !link.origin) {
|
||||
link = urlParse(baseUrl + '/' + url.replace(/^\//g, ''), {});
|
||||
}
|
||||
|
||||
return link.toString();
|
||||
}
|
||||
11
frontend/src/static/js/utils/helpers/formatInnerLink.ts
Executable file
11
frontend/src/static/js/utils/helpers/formatInnerLink.ts
Executable file
@@ -0,0 +1,11 @@
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
export function formatInnerLink(url: string, baseUrl: string) {
|
||||
let link = urlParse(url, {});
|
||||
|
||||
if ('' === link.origin || 'null' === link.origin || !link.origin) {
|
||||
link = urlParse(baseUrl + '/' + url.replace(/^\//g, ''), {});
|
||||
}
|
||||
|
||||
return link.toString();
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { months as monthList } from '../constants/';
|
||||
|
||||
export function formatManagementTableDate(date) {
|
||||
const day = date.getDate();
|
||||
const month = monthList[date.getMonth()].substring(0, 3);
|
||||
const year = date.getFullYear();
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const seconds = date.getSeconds();
|
||||
let ret = month + ' ' + day + ', ' + year;
|
||||
ret += ' ' + (hours < 10 ? '0' : '') + hours;
|
||||
ret += ':' + (minutes < 10 ? '0' : '') + minutes;
|
||||
ret += ':' + (seconds < 10 ? '0' : '') + seconds;
|
||||
return ret;
|
||||
}
|
||||
15
frontend/src/static/js/utils/helpers/formatManagementTableDate.ts
Executable file
15
frontend/src/static/js/utils/helpers/formatManagementTableDate.ts
Executable file
@@ -0,0 +1,15 @@
|
||||
import { months as monthList } from '../constants';
|
||||
|
||||
export function formatManagementTableDate(date: Date) {
|
||||
const day = date.getDate();
|
||||
const month = monthList[date.getMonth()].substring(0, 3);
|
||||
const year = date.getFullYear();
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const seconds = date.getSeconds();
|
||||
let ret = month + ' ' + day + ', ' + year;
|
||||
ret += ' ' + (hours < 10 ? '0' : '') + hours;
|
||||
ret += ':' + (minutes < 10 ? '0' : '') + minutes;
|
||||
ret += ':' + (seconds < 10 ? '0' : '') + seconds;
|
||||
return ret;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
export default function (views_number, fullNumber) {
|
||||
function formattedValue(val, lim, unit) {
|
||||
return Number(parseFloat(val / lim).toFixed(val < 10 * lim ? 1 : 0)) + unit;
|
||||
}
|
||||
|
||||
function format(i, views, mult, compare, limit, units) {
|
||||
while (views >= compare) {
|
||||
limit *= mult;
|
||||
compare *= mult;
|
||||
i += 1;
|
||||
}
|
||||
return i < units.length
|
||||
? formattedValue(views, limit, units[i])
|
||||
: formattedValue(views * (mult * (i - (units.length - 1))), limit, units[units.length - 1]);
|
||||
}
|
||||
|
||||
return fullNumber ? views_number.toLocaleString() : format(0, views_number, 1000, 1000, 1, ['', 'K', 'M', 'B', 'T']);
|
||||
}
|
||||
17
frontend/src/static/js/utils/helpers/formatViewsNumber.ts
Executable file
17
frontend/src/static/js/utils/helpers/formatViewsNumber.ts
Executable file
@@ -0,0 +1,17 @@
|
||||
const formattedValue = (val: number, lim: number, unit: string) =>
|
||||
Number((val / lim).toFixed(val < 10 * lim ? 1 : 0)) + unit;
|
||||
|
||||
function format(cntr: number, views: number, mult: number, compare: number, limit: number, units: string[]) {
|
||||
let i = cntr;
|
||||
while (views >= compare) {
|
||||
limit *= mult;
|
||||
compare *= mult;
|
||||
i += 1;
|
||||
}
|
||||
return i < units.length
|
||||
? formattedValue(views, limit, units[i])
|
||||
: formattedValue(views * (mult * (i - (units.length - 1))), limit, units[units.length - 1]);
|
||||
}
|
||||
|
||||
export const formatViewsNumber = (views_number: number, fullNumber?: boolean) =>
|
||||
fullNumber ? views_number.toLocaleString() : format(0, views_number, 1000, 1000, 1, ['', 'K', 'M', 'B', 'T']);
|
||||
@@ -1,7 +0,0 @@
|
||||
export const imageExtension = (img) => {
|
||||
if (!img) {
|
||||
return;
|
||||
}
|
||||
const ext = img.split('.');
|
||||
return ext[ext.length - 1];
|
||||
};
|
||||
5
frontend/src/static/js/utils/helpers/imageExtension.ts
Executable file
5
frontend/src/static/js/utils/helpers/imageExtension.ts
Executable file
@@ -0,0 +1,5 @@
|
||||
export const imageExtension = (img: string) => {
|
||||
if (img) {
|
||||
return img.split('.').pop();
|
||||
}
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
export * from './dom';
|
||||
export * from './errors';
|
||||
export { default as exportStore } from './exportStore';
|
||||
export { formatInnerLink } from './formatInnerLink';
|
||||
export * from './formatManagementTableDate';
|
||||
export { default as formatViewsNumber } from './formatViewsNumber';
|
||||
export * from './csrfToken';
|
||||
export { imageExtension } from './imageExtension';
|
||||
export * from './log';
|
||||
export * from './math';
|
||||
export * from './propTypeFilters';
|
||||
export { default as publishedOnDate } from './publishedOnDate';
|
||||
export * from './quickSort';
|
||||
export * from './requests';
|
||||
export { translateString } from './translate';
|
||||
export { replaceString } from './replacementStrings';
|
||||
export * from './embeddedApp';
|
||||
17
frontend/src/static/js/utils/helpers/index.ts
Normal file
17
frontend/src/static/js/utils/helpers/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export * from './csrfToken';
|
||||
export * from './dom';
|
||||
export * from './embeddedApp';
|
||||
export * from './errors';
|
||||
export * from './exportStore';
|
||||
export * from './formatInnerLink';
|
||||
export * from './formatManagementTableDate';
|
||||
export * from './formatViewsNumber';
|
||||
export * from './imageExtension';
|
||||
export * from './log';
|
||||
export * from './math';
|
||||
export * from './propTypeFilters';
|
||||
export * from './publishedOnDate';
|
||||
export * from './quickSort';
|
||||
export * from './requests';
|
||||
export * from './translate';
|
||||
export * from './replacementStrings';
|
||||
@@ -1,4 +0,0 @@
|
||||
const log = (...x) => console[x[0]](...x.slice(1));
|
||||
|
||||
export const warn = (...x) => log('warn', ...x);
|
||||
export const error = (...x) => log('error', ...x);
|
||||
9
frontend/src/static/js/utils/helpers/log.ts
Executable file
9
frontend/src/static/js/utils/helpers/log.ts
Executable file
@@ -0,0 +1,9 @@
|
||||
// @todo: Delete this file
|
||||
|
||||
export const warn = (...x: string[]) => {
|
||||
console.warn(...x);
|
||||
};
|
||||
|
||||
export const error = (...x: string[]) => {
|
||||
console.error(...x);
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
export const isGt = (x, y) => x > y;
|
||||
export const isZero = (x) => 0 === x;
|
||||
export const isNumber = (x) => !isNaN(x) && x === 0 + x;
|
||||
export const isInteger = (x) => x === Math.trunc(x);
|
||||
export const isPositive = (x) => isGt(x, 0);
|
||||
export const isPositiveNumber = (x) => isNumber(x) && isPositive(x);
|
||||
export const isPositiveInteger = (x) => isInteger(x) && isPositive(x);
|
||||
export const isPositiveIntegerOrZero = (x) => isInteger(x) && (isPositive(x) || isZero(x));
|
||||
|
||||
export const greaterCommonDivision = (a, b) => (!b ? a : greaterCommonDivision(b, a % b));
|
||||
10
frontend/src/static/js/utils/helpers/math.ts
Executable file
10
frontend/src/static/js/utils/helpers/math.ts
Executable file
@@ -0,0 +1,10 @@
|
||||
export const isGt = (x: number, y: number) => x > y;
|
||||
export const isZero = (x: number) => 0 === x;
|
||||
export const isNumber = (x: number) => 'number' === typeof x && !Number.isNaN(x);
|
||||
export const isInteger = (x: number) => x === Math.trunc(x);
|
||||
export const isPositive = (x: number) => isGt(x, 0);
|
||||
export const isPositiveNumber = (x: number) => isNumber(x) && isPositive(x);
|
||||
export const isPositiveInteger = (x: number) => isInteger(x) && isPositive(x);
|
||||
export const isPositiveIntegerOrZero = (x: number) => isInteger(x) && (isPositive(x) || isZero(x));
|
||||
|
||||
export const greaterCommonDivision = (a: number, b: number): number => (!b ? a : greaterCommonDivision(b, a % b));
|
||||
@@ -1,10 +1,10 @@
|
||||
import { logErrorAndReturnError } from './errors';
|
||||
import { isPositiveInteger, isPositiveIntegerOrZero } from './math';
|
||||
|
||||
// @todo: Check this
|
||||
export const PositiveIntegerOrZero = (function () {
|
||||
const isPositiveIntegerOrZero = (x) => x === Math.trunc(x) && x >= 0;
|
||||
|
||||
return function (obj, key, comp) {
|
||||
return void 0 === obj[key] || isPositiveIntegerOrZero(obj[key])
|
||||
return function (obj: Record<string, number>, key: string, comp: string) {
|
||||
return obj[key] === undefined || isPositiveIntegerOrZero(obj[key])
|
||||
? null
|
||||
: logErrorAndReturnError([
|
||||
'Invalid prop `' +
|
||||
@@ -20,11 +20,10 @@ export const PositiveIntegerOrZero = (function () {
|
||||
};
|
||||
})();
|
||||
|
||||
// @todo: Check this
|
||||
export const PositiveInteger = (function () {
|
||||
const isPositiveInteger = (x) => x === Math.trunc(x) && x > 0;
|
||||
|
||||
return function (obj, key, comp) {
|
||||
return void 0 === obj[key] || isPositiveInteger(obj[key])
|
||||
return function (obj: Record<string, number>, key: string, comp: string) {
|
||||
return obj[key] === undefined || isPositiveInteger(obj[key])
|
||||
? null
|
||||
: logErrorAndReturnError([
|
||||
'Invalid prop `' +
|
||||
@@ -1,17 +0,0 @@
|
||||
import { months } from '../constants';
|
||||
|
||||
export default function publishedOnDate(date, type) {
|
||||
if (date instanceof Date) {
|
||||
type = 0 + type;
|
||||
type = 0 < type ? type : 1;
|
||||
switch (type) {
|
||||
case 1:
|
||||
return months[date.getMonth()].substring(0, 3) + ' ' + date.getDate() + ', ' + date.getFullYear();
|
||||
case 2:
|
||||
return date.getDate() + ' ' + months[date.getMonth()].substring(0, 3) + ' ' + date.getFullYear();
|
||||
case 3:
|
||||
return date.getDate() + ' ' + months[date.getMonth()] + ' ' + date.getFullYear();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
17
frontend/src/static/js/utils/helpers/publishedOnDate.ts
Executable file
17
frontend/src/static/js/utils/helpers/publishedOnDate.ts
Executable file
@@ -0,0 +1,17 @@
|
||||
import { months } from '../constants';
|
||||
|
||||
export function publishedOnDate(date: Date, type: 1 | 2 | 3 = 1) {
|
||||
if (!(date instanceof Date)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type === 2) {
|
||||
return date.getDate() + ' ' + months[date.getMonth()].substring(0, 3) + ' ' + date.getFullYear();
|
||||
}
|
||||
|
||||
if (type === 3) {
|
||||
return date.getDate() + ' ' + months[date.getMonth()] + ' ' + date.getFullYear();
|
||||
}
|
||||
|
||||
return months[date.getMonth()].substring(0, 3) + ' ' + date.getDate() + ', ' + date.getFullYear();
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
function swap(arr, i, j) {
|
||||
var temp = arr[i];
|
||||
arr[i] = arr[j];
|
||||
arr[j] = temp;
|
||||
}
|
||||
|
||||
function partition(arr, pivot, left, right) {
|
||||
var pivotValue = arr[pivot],
|
||||
partitionIndex = left;
|
||||
|
||||
for (var i = left; i < right; i++) {
|
||||
if (arr[i] < pivotValue) {
|
||||
swap(arr, i, partitionIndex);
|
||||
partitionIndex++;
|
||||
}
|
||||
}
|
||||
swap(arr, right, partitionIndex);
|
||||
return partitionIndex;
|
||||
}
|
||||
|
||||
export function quickSort(arr, left, right) {
|
||||
var len = arr.length,
|
||||
pivot,
|
||||
partitionIndex;
|
||||
|
||||
if (left < right) {
|
||||
pivot = right;
|
||||
partitionIndex = partition(arr, pivot, left, right);
|
||||
|
||||
//sort left and right
|
||||
quickSort(arr, left, partitionIndex - 1);
|
||||
quickSort(arr, partitionIndex + 1, right);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
29
frontend/src/static/js/utils/helpers/quickSort.ts
Executable file
29
frontend/src/static/js/utils/helpers/quickSort.ts
Executable file
@@ -0,0 +1,29 @@
|
||||
function swap(arr: unknown[], i: number, j: number) {
|
||||
const temp = arr[i];
|
||||
arr[i] = arr[j];
|
||||
arr[j] = temp;
|
||||
}
|
||||
|
||||
function partition(arr: number[], pivot: number, left: number, right: number) {
|
||||
const pivotValue = arr[pivot];
|
||||
let partitionIndex = left;
|
||||
for (let i = left; i < right; i++) {
|
||||
if (arr[i] < pivotValue) {
|
||||
swap(arr, i, partitionIndex);
|
||||
partitionIndex++;
|
||||
}
|
||||
}
|
||||
swap(arr, right, partitionIndex);
|
||||
return partitionIndex;
|
||||
}
|
||||
|
||||
export function quickSort(arr: number[], left: number, right: number) {
|
||||
if (left < right) {
|
||||
const pivot = right;
|
||||
const partitionIndex = partition(arr, pivot, left, right);
|
||||
//sort left and right
|
||||
quickSort(arr, left, partitionIndex - 1);
|
||||
quickSort(arr, partitionIndex + 1, right);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// check templates/config/installation/translations.html for more
|
||||
|
||||
export function replaceString(word) {
|
||||
if (!window.REPLACEMENTS) {
|
||||
return word;
|
||||
}
|
||||
|
||||
let result = word;
|
||||
|
||||
for (const [search, replacement] of Object.entries(window.REPLACEMENTS)) {
|
||||
result = result.split(search).join(replacement);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
47
frontend/src/static/js/utils/helpers/replacementStrings.ts
Normal file
47
frontend/src/static/js/utils/helpers/replacementStrings.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// check templates/config/installation/translations.html for more
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
REPLACEMENTS?: Record<string, string>;
|
||||
}
|
||||
}
|
||||
|
||||
export function replaceString(word: string) {
|
||||
if (!window.REPLACEMENTS) {
|
||||
return word;
|
||||
}
|
||||
|
||||
let result = word;
|
||||
|
||||
for (const [search, replacement] of Object.entries(window.REPLACEMENTS)) {
|
||||
result = result.split(search).join(replacement);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// @todo: Check this alterative.
|
||||
/*function replaceStringRegExp(word: string) {
|
||||
if (!window.REPLACEMENTS) {
|
||||
return word;
|
||||
}
|
||||
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
let result = word;
|
||||
|
||||
for (const [search, replacement] of Object.entries(window.REPLACEMENTS)) {
|
||||
const regex = new RegExp(escapeRegExp(search), 'g');
|
||||
result = result.replace(regex, replacement);
|
||||
}
|
||||
|
||||
return result;
|
||||
}*/
|
||||
|
||||
// @todo: Remove older vesion.
|
||||
/*export function replaceString_OLD(string: string) {
|
||||
for (const key in window.REPLACEMENTS) {
|
||||
string = string.replace(key, window.REPLACEMENTS[key]);
|
||||
}
|
||||
return string;
|
||||
}*/
|
||||
@@ -1,135 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export async function getRequest(url, sync, callback, errorCallback) {
|
||||
const requestConfig = {
|
||||
timeout: null,
|
||||
maxContentLength: null,
|
||||
};
|
||||
|
||||
function responseHandler(result) {
|
||||
if (callback instanceof Function || typeof callback === 'function') {
|
||||
callback(result);
|
||||
}
|
||||
}
|
||||
|
||||
function errorHandler(error) {
|
||||
if (errorCallback instanceof Function || typeof errorCallback === 'function') {
|
||||
let err = error;
|
||||
if (void 0 === error.response) {
|
||||
err = {
|
||||
type: 'network',
|
||||
error: error,
|
||||
};
|
||||
} else if (void 0 !== error.response.status) {
|
||||
// TODO: Improve this, it's valid only in case of media requests.
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
err = {
|
||||
type: 'private',
|
||||
error: error,
|
||||
message: 'Media is private',
|
||||
};
|
||||
break;
|
||||
case 400:
|
||||
err = {
|
||||
type: 'unavailable',
|
||||
error: error,
|
||||
message: 'Media is unavailable',
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
errorCallback(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
await axios.get(url, requestConfig)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
} else {
|
||||
axios.get(url, requestConfig)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
}
|
||||
}
|
||||
|
||||
export async function postRequest(url, postData, configData, sync, callback, errorCallback) {
|
||||
postData = postData || {};
|
||||
|
||||
function responseHandler(result) {
|
||||
if (callback instanceof Function || typeof callback === 'function') {
|
||||
callback(result);
|
||||
}
|
||||
}
|
||||
|
||||
function errorHandler(error) {
|
||||
if (errorCallback instanceof Function || typeof errorCallback === 'function') {
|
||||
errorCallback(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
await axios.post(url, postData, configData || null)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
} else {
|
||||
axios.post(url, postData, configData || null)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
}
|
||||
}
|
||||
|
||||
export async function putRequest(url, putData, configData, sync, callback, errorCallback) {
|
||||
putData = putData || {};
|
||||
|
||||
function responseHandler(result) {
|
||||
if (callback instanceof Function || typeof callback === 'function') {
|
||||
callback(result);
|
||||
}
|
||||
}
|
||||
|
||||
function errorHandler(error) {
|
||||
if (errorCallback instanceof Function || typeof errorCallback === 'function') {
|
||||
errorCallback(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
await axios.put(url, putData, configData || null)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
} else {
|
||||
axios.put(url, putData, configData || null)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRequest(url, configData, sync, callback, errorCallback) {
|
||||
configData = configData || {};
|
||||
|
||||
function responseHandler(result) {
|
||||
if (callback instanceof Function || typeof callback === 'function') {
|
||||
callback(result);
|
||||
}
|
||||
}
|
||||
|
||||
function errorHandler(error) {
|
||||
if (errorCallback instanceof Function || typeof errorCallback === 'function') {
|
||||
errorCallback(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
await axios
|
||||
.delete(url, configData || null)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
} else {
|
||||
axios
|
||||
.delete(url, configData || null)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
}
|
||||
}
|
||||
169
frontend/src/static/js/utils/helpers/requests.ts
Normal file
169
frontend/src/static/js/utils/helpers/requests.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
export async function getRequest(
|
||||
url: string,
|
||||
sync: boolean = false,
|
||||
callback?: (response: AxiosResponse<any, any, {}>) => void,
|
||||
errorCallback?: (err: any) => void
|
||||
) {
|
||||
const requestConfig = {
|
||||
timeout: undefined,
|
||||
maxContentLength: undefined,
|
||||
};
|
||||
|
||||
function responseHandler(result: AxiosResponse<any, any, {}>) {
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
}
|
||||
|
||||
function errorHandler(reason: any) {
|
||||
if (!errorCallback) {
|
||||
return;
|
||||
}
|
||||
|
||||
let err = reason;
|
||||
if (reason.response === undefined) {
|
||||
err = {
|
||||
type: 'network',
|
||||
error: reason,
|
||||
};
|
||||
} else if (reason.response.status !== undefined) {
|
||||
// @todo: Improve this, it's valid only in case of media requests.
|
||||
switch (reason.response.status) {
|
||||
case 401:
|
||||
err = {
|
||||
type: 'private',
|
||||
error: reason,
|
||||
message: 'Media is private',
|
||||
};
|
||||
break;
|
||||
case 400:
|
||||
err = {
|
||||
type: 'unavailable',
|
||||
error: reason,
|
||||
message: 'Media is unavailable',
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
errorCallback(err);
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
await axios
|
||||
.get(url, requestConfig)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
} else {
|
||||
axios
|
||||
.get(url, requestConfig)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
}
|
||||
}
|
||||
|
||||
export async function postRequest(
|
||||
url: string,
|
||||
postData: any,
|
||||
configData?: AxiosRequestConfig<any>,
|
||||
sync: boolean = false,
|
||||
callback?: (response: AxiosResponse<any, any, {}>) => void,
|
||||
errorCallback?: (error: any) => void
|
||||
) {
|
||||
postData = postData || {};
|
||||
|
||||
function responseHandler(result: AxiosResponse<any, any, {}>) {
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
}
|
||||
|
||||
function errorHandler(error: any) {
|
||||
if (errorCallback) {
|
||||
errorCallback(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
await axios
|
||||
.post(url, postData, configData)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
} else {
|
||||
axios
|
||||
.post(url, postData, configData)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
}
|
||||
}
|
||||
|
||||
export async function putRequest(
|
||||
url: string,
|
||||
putData: any,
|
||||
configData?: AxiosRequestConfig<any>,
|
||||
sync: boolean = false,
|
||||
callback?: (response: AxiosResponse<any, any, {}>) => void,
|
||||
errorCallback?: (error: any) => void
|
||||
) {
|
||||
putData = putData || {};
|
||||
|
||||
function responseHandler(result: AxiosResponse<any, any, {}>) {
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
}
|
||||
|
||||
function errorHandler(error: any) {
|
||||
if (errorCallback) {
|
||||
errorCallback(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
await axios
|
||||
.put(url, putData, configData)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
} else {
|
||||
axios
|
||||
.put(url, putData, configData)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRequest(
|
||||
url: string,
|
||||
configData?: AxiosRequestConfig<any>,
|
||||
sync: boolean = false,
|
||||
callback?: (response: AxiosResponse<any, any, {}>) => void,
|
||||
errorCallback?: (error: any) => void
|
||||
) {
|
||||
configData = configData || {};
|
||||
|
||||
function responseHandler(result: AxiosResponse<any, any, {}>) {
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
}
|
||||
|
||||
function errorHandler(error: any) {
|
||||
if (errorCallback) {
|
||||
errorCallback(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (sync) {
|
||||
await axios
|
||||
.delete(url, configData)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
} else {
|
||||
axios
|
||||
.delete(url, configData || null)
|
||||
.then(responseHandler)
|
||||
.catch(errorHandler || null);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// check templates/config/installation/translations.html for more
|
||||
|
||||
export function translateString(str) {
|
||||
return window.TRANSLATION?.[str] ?? str;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user