Compare commits

...

19 Commits

Author SHA1 Message Date
Yiannis
20682a543a chore: minor code enhancements 2026-03-21 17:17:55 +02:00
Yiannis
c8b47a7922 refactor(frontend): convert contexts layer to TS and align page integration 2026-03-11 02:52:51 +02:00
Yiannis
499196b0f6 chore(frontend): harden settings parsing and update store imports 2026-03-11 02:31:07 +02:00
Yiannis
374ae4de6e refactor(frontend): replace legacy settings init/settings pattern with typed config functions 2026-03-11 02:14:45 +02:00
Yiannis
7a5fca6fd8 refactor(frontend): replace legacy action files with TypeScript equivalents 2026-03-11 02:06:59 +02:00
Yiannis
e9af15582f feat(types): create typed schema for global cms and runtime config 2026-03-11 01:56:54 +02:00
Yiannis
1b8e8aae6a refactor(frontend): replace legacy utils JS files with typed TS equivalents 2026-03-11 01:51:53 +02:00
Yiannis
df4b0422d5 fix(version): bump VERSION to 7.9 after accidental downgrade 2026-03-08 02:31:25 +02:00
Yiannis
0434f24691 chore(frontend): update frontend/src/static (generated by make build-frontend) 2026-03-08 02:23:26 +02:00
Yiannis
c2043fafa1 feat: utils/hooks unit tests 2026-02-07 18:39:24 +02:00
Yiannis
9f9dd699b2 feat: utils/stores unit tests 2026-02-07 18:09:46 +02:00
Yiannis
e2bc9399b9 feat: utils/classes unit tests 2026-02-07 18:09:46 +02:00
Yiannis
45d94069b9 feat: utils/actions unit tests 2026-02-07 18:09:46 +02:00
semantic-release-bot
b7427869b6 chore(release): 7.6.0 [skip ci]
## [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](11449c2187))
2026-02-07 10:31:40 +00:00
LabPixel
11449c2187 feat: Create SECURITY.md (#1485) 2026-02-07 12:31:10 +02:00
semantic-release-bot
f7c675596f chore(release): 7.5.0 [skip ci]
## [7.5.0](https://github.com/mediacms-io/mediacms/compare/v7.4.0...v7.5.0) (2026-02-06)

### Features

* bump version ([36d815c](36d815c0cf))
2026-02-06 17:26:12 +00:00
Markos Gogoulos
36d815c0cf feat: bump version 2026-02-06 19:25:31 +02:00
semantic-release-bot
8f28b00a63 chore(release): 7.4.0 [skip ci]
## [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](74952f68d7))
2026-02-06 17:24:27 +00:00
Yiannis Christodoulou
74952f68d7 feat: Add video player context menu with share/embed options (#1472) 2026-02-06 19:23:51 +02:00
228 changed files with 10976 additions and 3757 deletions

View File

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

View File

@@ -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
View 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.

View File

@@ -1 +1 @@
VERSION = "7.7"
VERSION = "7.9"

View File

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

View File

@@ -204,6 +204,54 @@ class SeekIndicator extends Component {
</div>
`;
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);
}
}

View File

@@ -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';

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useMemo } from 'react';
import React, { useEffect, useRef, useMemo, useState, useCallback } from 'react';
import videojs from 'video.js';
import '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}
/>
</>
);
}

View File

@@ -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()) {

View File

@@ -1,3 +1,4 @@
{
"editor.formatOnSave": true
}
"editor.formatOnSave": true,
"prettier.configPath": "../.prettierrc"
}

View File

@@ -5,5 +5,5 @@ module.exports = {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.jsx?$': 'babel-jest',
},
collectCoverageFrom: ['src/**'],
collectCoverageFrom: ['src/**', '!src/static/lib/**'],
};

View File

@@ -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",

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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%;
}

View File

@@ -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,
};

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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}

View File

@@ -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;
}

View File

@@ -0,0 +1 @@
export type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;

View 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;
};

View 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;
};

View File

@@ -0,0 +1,3 @@
export * from './DeepPartial';
export * from './GlobalMediaCMS';
export * from './MediaCMSConfig';

View File

@@ -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,
});
}

View 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 });
}

View File

@@ -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,
});
}

View 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 });
}

View File

@@ -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,
});
}

View 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 });
}

View File

@@ -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',
});
}

View 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' });
}

View File

@@ -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',
});
}

View 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' });
}

View File

@@ -1,8 +0,0 @@
import Dispatcher from '../dispatcher.js';
export function requestPredictions(query) {
Dispatcher.dispatch({
type: 'REQUEST_PREDICTIONS',
query,
});
}

View File

@@ -0,0 +1,5 @@
import { dispatcher } from '../dispatcher';
export function requestPredictions(query: string) {
dispatcher.dispatch({ type: 'REQUEST_PREDICTIONS', query });
}

View File

@@ -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,
});
}

View 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 });
}

View File

@@ -1,2 +0,0 @@
export { default as months } from './months';
export { default as weekdays } from './weekdays';

View File

@@ -0,0 +1,2 @@
export * from './months';
export * from './weekdays';

View File

@@ -1,14 +0,0 @@
export default [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];

View File

@@ -0,0 +1,14 @@
export const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
] as const;

View File

@@ -1 +0,0 @@
export default ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

View File

@@ -0,0 +1 @@
export const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] as const;

View File

@@ -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;

View 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;

View File

@@ -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;

View 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;

View File

@@ -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]);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -0,0 +1,4 @@
import { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config';
export const PlaylistsContext = createContext(mediacmsConfig(window.MediaCMS).playlists);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View 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;

View File

@@ -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;

View 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;

View File

@@ -1,2 +0,0 @@
const Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();

View File

@@ -0,0 +1,3 @@
import { Dispatcher } from 'flux';
export const dispatcher = new Dispatcher();

View File

@@ -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;
}

View 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;
}

View File

@@ -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,
};
}

View 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,
};
}

View File

@@ -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;

View File

@@ -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);
}

View 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;
}

View File

@@ -1,5 +0,0 @@
import * as dispatcher from '../dispatcher.js';
export default function (store, handler) {
dispatcher.register(store[handler].bind(store));
return store;
}

View 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;
// }

View File

@@ -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();
}

View 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();
}

View File

@@ -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;
}

View 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;
}

View File

@@ -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']);
}

View 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']);

View File

@@ -1,7 +0,0 @@
export const imageExtension = (img) => {
if (!img) {
return;
}
const ext = img.split('.');
return ext[ext.length - 1];
};

View File

@@ -0,0 +1,5 @@
export const imageExtension = (img: string) => {
if (img) {
return img.split('.').pop();
}
};

View File

@@ -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';

View 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';

View File

@@ -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);

View 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);
};

View File

@@ -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));

View 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));

View File

@@ -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 `' +

View File

@@ -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;
}

View 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();
}

View File

@@ -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;
}

View 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;
}

View File

@@ -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;
}

View 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;
}*/

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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