Compare commits

...

10 Commits

Author SHA1 Message Date
Markos Gogoulos
0cdb3a2b3f fix 2025-10-27 12:51:59 +02:00
Markos Gogoulos
e017d7c03b fix 2025-10-27 12:34:42 +02:00
Markos Gogoulos
0da8fd6355 fix 2025-10-27 12:27:53 +02:00
Markos Gogoulos
0d8a81ff11 fix 2025-10-27 12:17:32 +02:00
Markos Gogoulos
bd7192d608 fix 2025-10-27 12:09:27 +02:00
Markos Gogoulos
a09096b78a fix 2025-10-27 11:57:08 +02:00
Markos Gogoulos
0b045cb2f3 fix 2025-10-27 11:56:35 +02:00
Yiannis Christodoulou
2a0cb977f2
Video.js fixes and improvements after major upgrade (#1413) 2025-10-27 11:53:35 +02:00
Markos Gogoulos
028a776269 fix 2025-10-27 11:46:45 +02:00
Markos Gogoulos
603fe16fca fix 2025-10-27 11:45:26 +02:00
42 changed files with 556 additions and 6146 deletions

View File

@ -100,6 +100,9 @@ RELATED_MEDIA_STRATEGY = "content"
# Whether or not to generate a sitemap.xml listing the pages on the site (default: False)
GENERATE_SITEMAP = False
# Whether to include media count numbers on categories and tags listing pages
INCLUDE_LISTING_NUMBERS = True
USE_I18N = True
USE_L10N = True
USE_TZ = True

View File

@ -1 +1 @@
VERSION = "11.555.0"
VERSION = "7.2.3"

View File

@ -538,6 +538,22 @@ By default `CAN_SEE_MEMBERS_PAGE = "all"` means that all registered users can se
By default, users do not require approval, so they can login immediately after registration (if registration is open). However, if the parameter `USERS_NEEDS_TO_BE_APPROVED` is set to `True`, they will first have to have their accounts approved by an administrator before they can successfully sign in.
Administrators can approve users through the following ways: 1. through Django administration, 2. through the users management page, 3. through editing the profile page directly. In all cases, set 'Is approved' to True.
### 5.29 Show or hide media count numbers on categories and tags pages
By default, the number of media items is displayed next to each category and tag on the `/categories` and `/tags` pages. To hide these numbers:
```
INCLUDE_LISTING_NUMBERS = False
```
To show the numbers (default behavior):
```
INCLUDE_LISTING_NUMBERS = True
```
This setting affects only the visual display on the categories and tags listing pages and does not impact the functionality of filtering by categories or tags.
## 6. Manage pages
to be written

View File

@ -57,6 +57,7 @@ def stuff(request):
ret["USE_SAML"] = settings.USE_SAML
ret["USE_RBAC"] = settings.USE_RBAC
ret["USE_ROUNDED_CORNERS"] = settings.USE_ROUNDED_CORNERS
ret["INCLUDE_LISTING_NUMBERS"] = settings.INCLUDE_LISTING_NUMBERS
ret["VERSION"] = VERSION
if request.user.is_superuser:

View File

@ -343,6 +343,10 @@ def publish_media(request):
if not (request.user.has_contributor_access_to_media(media) or is_mediacms_editor(request.user)):
return HttpResponseRedirect("/")
if not (request.user.has_owner_access_to_media(media) or is_mediacms_editor(request.user)):
messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, f"Permission to publish is not grated by the owner: {media.user.name}"))
return HttpResponseRedirect(media.get_absolute_url())
if request.method == "POST":
form = MediaPublishForm(request.user, request.POST, request.FILES, instance=media)
if form.is_valid():

View File

@ -96,7 +96,8 @@ const App = () => {
case 'ArrowLeft':
event.preventDefault();
if (videoRef.current) {
const newTime = Math.max(currentTime - 10, 0);
// Use the video element's current time directly to avoid stale state
const newTime = Math.max(videoRef.current.currentTime - 10, 0);
handleMobileSafeSeek(newTime);
logger.debug('Jumped backward 10 seconds to:', formatDetailedTime(newTime));
}
@ -104,7 +105,8 @@ const App = () => {
case 'ArrowRight':
event.preventDefault();
if (videoRef.current) {
const newTime = Math.min(currentTime + 10, duration);
// Use the video element's current time directly to avoid stale state
const newTime = Math.min(videoRef.current.currentTime + 10, duration);
handleMobileSafeSeek(newTime);
logger.debug('Jumped forward 10 seconds to:', formatDetailedTime(newTime));
}
@ -117,7 +119,7 @@ const App = () => {
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handlePlay, handleMobileSafeSeek, currentTime, duration, videoRef]);
}, [handlePlay, handleMobileSafeSeek, duration, videoRef]);
return (
<div className="bg-background min-h-screen">

View File

@ -589,12 +589,13 @@ const TimelineControls = ({
// Update display time and check for transitions between segments and empty spaces
useEffect(() => {
// Always update display time to match current video time when playing
// Always update display time to match current video time
if (videoRef.current) {
// If video is playing, always update the displayed time in the tooltip
if (!videoRef.current.paused) {
setDisplayTime(currentTime);
// Always update display time when current time changes (both playing and paused)
setDisplayTime(currentTime);
// If video is playing, also update the tooltip and perform segment checks
if (!videoRef.current.paused) {
// Also update clicked time to keep them in sync when playing
// This ensures correct time is shown when pausing
setClickedTime(currentTime);

View File

@ -253,7 +253,8 @@ const App = () => {
case 'ArrowLeft':
event.preventDefault();
if (videoRef.current) {
const newTime = Math.max(currentTime - 10, 0);
// Use the video element's current time directly to avoid stale state
const newTime = Math.max(videoRef.current.currentTime - 10, 0);
handleMobileSafeSeek(newTime);
logger.debug('Jumped backward 10 seconds to:', formatDetailedTime(newTime));
}
@ -261,7 +262,8 @@ const App = () => {
case 'ArrowRight':
event.preventDefault();
if (videoRef.current) {
const newTime = Math.min(currentTime + 10, duration);
// Use the video element's current time directly to avoid stale state
const newTime = Math.min(videoRef.current.currentTime + 10, duration);
handleMobileSafeSeek(newTime);
logger.debug('Jumped forward 10 seconds to:', formatDetailedTime(newTime));
}
@ -274,7 +276,7 @@ const App = () => {
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handlePlay, handleMobileSafeSeek, currentTime, duration, videoRef]);
}, [handlePlay, handleMobileSafeSeek, duration, videoRef]);
return (
<div className="bg-background min-h-screen">

View File

@ -779,12 +779,13 @@ const TimelineControls = ({
// Update display time and check for transitions between segments and empty spaces
useEffect(() => {
// Always update display time to match current video time when playing
// Always update display time to match current video time
if (videoRef.current) {
// If video is playing, always update the displayed time in the tooltip
if (!videoRef.current.paused) {
setDisplayTime(currentTime);
// Always update display time when current time changes (both playing and paused)
setDisplayTime(currentTime);
// If video is playing, also update the tooltip and perform segment checks
if (!videoRef.current.paused) {
// Also update clicked time to keep them in sync when playing
// This ensures correct time is shown when pausing
setClickedTime(currentTime);

View File

@ -1,15 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VideoJS</title>
</head>
<body style="padding: 0; margin: 0">
<div id="page-embed">
<div id="video-js-root-embed-old" class="video-js-root-embed-old"></div>
</div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VideoJS</title>
</head>
<body style="padding: 0; margin: 0">
<div id="video-js-root-main-old" class="video-js-root-main-old"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -45,7 +45,7 @@ class EndScreenOverlay extends Component {
}
createGrid() {
const { columns, maxVideos, useSwiper } = this.getGridConfig();
const { columns, maxVideos, useSwiper, itemsPerView, gridRows } = this.getGridConfig();
// Get videos to show - access directly from options during createEl
const relatedVideos = this.options_?.relatedVideos || this.relatedVideos || [];
@ -55,7 +55,7 @@ class EndScreenOverlay extends Component {
: this.createSampleVideos().slice(0, maxVideos);
if (useSwiper) {
return this.createSwiperGrid(videosToShow);
return this.createSwiperGrid(videosToShow, itemsPerView || 2, columns, gridRows || 1);
} else {
return this.createRegularGrid(columns, videosToShow);
}
@ -91,14 +91,14 @@ class EndScreenOverlay extends Component {
return grid;
}
createSwiperGrid(videosToShow) {
createSwiperGrid(videosToShow, itemsPerView = 2, columns = 2, gridRows = 1) {
const container = videojs.dom.createEl('div', {
className: 'vjs-related-videos-swiper-container',
});
// Container styling - ensure it stays within bounds
container.style.position = 'relative';
container.style.padding = '20px';
container.style.padding = gridRows > 1 ? '12px' : '20px'; // Minimal padding for 2x2 grid
container.style.height = '100%';
container.style.width = '100%';
container.style.display = 'flex';
@ -111,16 +111,32 @@ class EndScreenOverlay extends Component {
className: 'vjs-related-videos-swiper',
});
swiperWrapper.style.display = 'flex';
swiperWrapper.style.overflowX = 'auto';
swiperWrapper.style.overflowY = 'hidden';
swiperWrapper.style.gap = '12px';
swiperWrapper.style.paddingBottom = '10px';
swiperWrapper.style.scrollBehavior = 'smooth';
swiperWrapper.style.scrollSnapType = 'x mandatory';
swiperWrapper.style.width = '100%';
swiperWrapper.style.maxWidth = '100%';
swiperWrapper.style.boxSizing = 'border-box';
if (gridRows > 1) {
// Multi-row grid layout (e.g., 2x2 for landscape)
swiperWrapper.style.display = 'flex';
swiperWrapper.style.overflowX = 'auto';
swiperWrapper.style.overflowY = 'hidden';
swiperWrapper.style.scrollBehavior = 'smooth';
swiperWrapper.style.scrollSnapType = 'x mandatory';
swiperWrapper.style.width = '100%';
swiperWrapper.style.maxWidth = '100%';
swiperWrapper.style.height = '100%';
swiperWrapper.style.flex = '1';
swiperWrapper.style.boxSizing = 'border-box';
swiperWrapper.style.gap = '0'; // Remove gap, we'll handle it in pages
} else {
// Single row layout (original swiper)
swiperWrapper.style.display = 'flex';
swiperWrapper.style.overflowX = 'auto';
swiperWrapper.style.overflowY = 'hidden';
swiperWrapper.style.gap = '12px';
swiperWrapper.style.paddingBottom = '10px';
swiperWrapper.style.scrollBehavior = 'smooth';
swiperWrapper.style.scrollSnapType = 'x mandatory';
swiperWrapper.style.width = '100%';
swiperWrapper.style.maxWidth = '100%';
swiperWrapper.style.boxSizing = 'border-box';
}
// Hide scrollbar and prevent scroll propagation
swiperWrapper.style.scrollbarWidth = 'none'; // Firefox
@ -158,17 +174,56 @@ class EndScreenOverlay extends Component {
{ passive: true }
);
// Create video items for swiper (show 2 at a time, but allow scrolling through all)
videosToShow.forEach((video) => {
const videoItem = this.createVideoItem(video, true); // Pass true for swiper mode
swiperWrapper.appendChild(videoItem);
});
if (gridRows > 1) {
// Create pages with grid layout (e.g., 2x2 grid per page)
const itemsPerPage = itemsPerView;
const totalPages = Math.ceil(videosToShow.length / itemsPerPage);
for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
const page = videojs.dom.createEl('div', {
className: 'vjs-swiper-page',
});
page.style.minWidth = '100%';
page.style.width = '100%';
page.style.height = '100%';
page.style.display = 'grid';
page.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
page.style.gridTemplateRows = `repeat(${gridRows}, 1fr)`;
page.style.gap = '12px'; // Increased gap for better spacing
page.style.scrollSnapAlign = 'start';
page.style.boxSizing = 'border-box';
page.style.alignContent = 'stretch';
page.style.justifyContent = 'stretch';
page.style.alignItems = 'stretch';
page.style.justifyItems = 'stretch';
// Get videos for this page
const startIndex = pageIndex * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, videosToShow.length);
const pageVideos = videosToShow.slice(startIndex, endIndex);
// Create video items for this page
pageVideos.forEach((video) => {
const videoItem = this.createVideoItem(video, true, itemsPerView, true); // Pass true for grid mode
page.appendChild(videoItem);
});
swiperWrapper.appendChild(page);
}
} else {
// Single row - create video items directly
videosToShow.forEach((video) => {
const videoItem = this.createVideoItem(video, true, itemsPerView, false);
swiperWrapper.appendChild(videoItem);
});
}
container.appendChild(swiperWrapper);
// Add navigation indicators if there are more than 2 videos
if (videosToShow.length > 2) {
const indicators = this.createSwiperIndicators(videosToShow.length, swiperWrapper);
// Add navigation indicators if there are more videos than can fit in one view
if (videosToShow.length > itemsPerView) {
const indicators = this.createSwiperIndicators(videosToShow.length, swiperWrapper, itemsPerView);
container.appendChild(indicators);
}
@ -190,10 +245,48 @@ class EndScreenOverlay extends Component {
// Calculate maximum rows that can fit - be more aggressive
const maxRows = Math.max(2, Math.floor((availableHeight + gap) / (cardHeight + gap)));
console.log('Grid Config:', { playerWidth, playerHeight, availableHeight, maxRows });
// Detect landscape orientation on mobile
// Check screen/window orientation first, then player dimensions
const screenWidth = window.innerWidth || document.documentElement.clientWidth;
const screenHeight = window.innerHeight || document.documentElement.clientHeight;
const isScreenLandscape = screenWidth > screenHeight;
const isLandscape = playerWidth > playerHeight;
// Detect mobile/touch devices - should always show swiper
// Check both width and touch capability for better detection
const isTouchDevice = this.isTouchDevice;
const isSmallScreen = screenWidth < 700 || playerWidth < 700;
const isMobileOrTouch = isTouchDevice || isSmallScreen;
// For mobile, prioritize screen orientation over player dimensions
// Only consider it landscape if BOTH screen and player are in landscape
const isDefinitelyLandscape = isMobileOrTouch ? isScreenLandscape && isLandscape : isLandscape;
console.log('Grid Config:', {
screenWidth,
screenHeight,
isScreenLandscape,
playerWidth,
playerHeight,
availableHeight,
maxRows,
isLandscape,
isDefinitelyLandscape,
isTouchDevice,
isSmallScreen,
isMobileOrTouch,
});
// Enhanced grid configuration to fill all available space
if (playerWidth >= 1600) {
// Check mobile/touch conditions first - swiper should ALWAYS be used on mobile/touch devices
if (isMobileOrTouch && isDefinitelyLandscape) {
// Mobile/Touch landscape: show 2x2 grid (4 items total) with swiper for pagination
return { columns: 2, maxVideos: 12, useSwiper: true, itemsPerView: 4, gridRows: 2 };
} else if (isMobileOrTouch) {
// Mobile/Touch portrait: show 2 items in single row swiper mode
return { columns: 2, maxVideos: 12, useSwiper: true, itemsPerView: 2, gridRows: 1 };
} else if (playerWidth >= 1600) {
const columns = 5;
return { columns, maxVideos: columns * maxRows, useSwiper: false }; // Fill all available rows
} else if (playerWidth >= 1200) {
@ -202,11 +295,9 @@ class EndScreenOverlay extends Component {
} else if (playerWidth >= 900) {
const columns = 3;
return { columns, maxVideos: columns * maxRows, useSwiper: false }; // Fill all available rows
} else if (playerWidth >= 700) {
} else {
const columns = 2;
return { columns, maxVideos: columns * maxRows, useSwiper: false }; // Fill all available rows
} else {
return { columns: 2, maxVideos: 12, useSwiper: true }; // Use swiper for small screens
}
}
@ -220,7 +311,7 @@ class EndScreenOverlay extends Component {
return this.createSampleVideos().slice(0, maxVideos);
}
createVideoItem(video, isSwiperMode = false) {
createVideoItem(video, isSwiperMode = false, itemsPerView = 2, isGridMode = false) {
const item = videojs.dom.createEl('div', {
className: `vjs-related-video-item ${isSwiperMode ? 'vjs-swiper-item' : ''}`,
});
@ -228,7 +319,7 @@ class EndScreenOverlay extends Component {
// Consistent item styling with fixed dimensions
item.style.position = 'relative';
item.style.backgroundColor = '#1a1a1a';
item.style.borderRadius = '6px';
item.style.borderRadius = '8px';
item.style.overflow = 'hidden';
item.style.cursor = 'pointer';
item.style.transition = 'transform 0.15s ease, box-shadow 0.15s ease';
@ -236,11 +327,21 @@ class EndScreenOverlay extends Component {
item.style.flexDirection = 'column';
// Consistent dimensions for all cards
if (isSwiperMode) {
// Calculate proper width for swiper items (2 items visible + gap)
item.style.minWidth = 'calc(50% - 6px)'; // 50% width minus half the gap
item.style.width = 'calc(50% - 6px)';
item.style.maxWidth = '180px'; // Maximum width for larger screens
if (isGridMode) {
// Grid mode (2x2): items fill their grid cell completely
item.style.height = '100%';
item.style.minHeight = '0';
item.style.width = '100%';
item.style.maxWidth = 'none';
item.style.flex = '1';
} else if (isSwiperMode) {
// Single row swiper mode: calculate width based on items per view
// Formula: (100% / itemsPerView) - (gap * (itemsPerView - 1) / itemsPerView)
const itemsPerRow = itemsPerView / (itemsPerView === 4 ? 2 : 1); // For 4 items in 2 rows, show 2 per row
const gapAdjustment = (12 * (itemsPerRow - 1)) / itemsPerRow;
item.style.minWidth = `calc(${100 / itemsPerRow}% - ${gapAdjustment}px)`;
item.style.width = `calc(${100 / itemsPerRow}% - ${gapAdjustment}px)`;
item.style.maxWidth = itemsPerView === 4 ? '150px' : '180px'; // Smaller max width for 4 items
// Simpler height since text is overlaid on thumbnail
const cardHeight = '120px'; // Just the thumbnail height
@ -270,7 +371,7 @@ class EndScreenOverlay extends Component {
}
// Create thumbnail container with overlaid text
const thumbnailContainer = this.createThumbnailWithOverlay(video, isSwiperMode);
const thumbnailContainer = this.createThumbnailWithOverlay(video, isSwiperMode, itemsPerView);
item.appendChild(thumbnailContainer);
console.log('Created video item with overlay:', item);
@ -331,7 +432,7 @@ class EndScreenOverlay extends Component {
return container;
}
createThumbnailWithOverlay(video, isSwiperMode = false) {
createThumbnailWithOverlay(video, isSwiperMode = false, itemsPerView = 2) {
const container = videojs.dom.createEl('div', {
className: 'vjs-related-video-thumbnail-container',
});
@ -339,9 +440,13 @@ class EndScreenOverlay extends Component {
// Container styling - full height since it contains everything
container.style.position = 'relative';
container.style.width = '100%';
container.style.height = '120px';
container.style.height = '100%';
container.style.minHeight = '120px';
container.style.overflow = 'hidden';
container.style.borderRadius = '6px';
container.style.borderRadius = '8px';
container.style.flex = '1';
container.style.display = 'flex';
container.style.flexDirection = 'column';
// Create thumbnail image
const thumbnail = videojs.dom.createEl('img', {
@ -354,6 +459,9 @@ class EndScreenOverlay extends Component {
thumbnail.style.height = '100%';
thumbnail.style.objectFit = 'cover';
thumbnail.style.display = 'block';
thumbnail.style.flex = '1';
thumbnail.style.minWidth = '0';
thumbnail.style.minHeight = '0';
container.appendChild(thumbnail);
@ -370,7 +478,7 @@ class EndScreenOverlay extends Component {
duration.style.color = 'white';
duration.style.padding = '2px 6px';
duration.style.borderRadius = '3px';
duration.style.fontSize = '11px';
duration.style.fontSize = itemsPerView === 4 ? '10px' : '11px';
duration.style.fontWeight = '600';
duration.style.lineHeight = '1';
duration.style.zIndex = '3';
@ -384,12 +492,12 @@ class EndScreenOverlay extends Component {
});
textOverlay.style.position = 'absolute';
textOverlay.style.top = '8px';
textOverlay.style.left = '8px';
textOverlay.style.right = '8px';
textOverlay.style.top = itemsPerView === 4 ? '6px' : '8px';
textOverlay.style.left = itemsPerView === 4 ? '6px' : '8px';
textOverlay.style.right = itemsPerView === 4 ? '6px' : '8px';
textOverlay.style.background =
'linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 70%, transparent 100%)';
textOverlay.style.padding = '8px';
textOverlay.style.padding = itemsPerView === 4 ? '6px' : '8px';
textOverlay.style.borderRadius = '4px';
textOverlay.style.zIndex = '2';
@ -399,10 +507,11 @@ class EndScreenOverlay extends Component {
});
title.textContent = video.title || 'Sample Video Title';
title.style.color = '#ffffff';
title.style.fontSize = isSwiperMode ? '12px' : '13px';
// Adjust font sizes based on items per view
title.style.fontSize = itemsPerView === 4 ? '11px' : isSwiperMode ? '12px' : '13px';
title.style.fontWeight = '600';
title.style.lineHeight = '1.3';
title.style.marginBottom = '4px';
title.style.marginBottom = itemsPerView === 4 ? '3px' : '4px';
title.style.overflow = 'hidden';
title.style.textOverflow = 'ellipsis';
title.style.display = '-webkit-box';
@ -428,7 +537,8 @@ class EndScreenOverlay extends Component {
meta.textContent = metaText;
meta.style.color = '#e0e0e0';
meta.style.fontSize = isSwiperMode ? '10px' : '11px';
// Adjust font sizes based on items per view
meta.style.fontSize = itemsPerView === 4 ? '9px' : isSwiperMode ? '10px' : '11px';
meta.style.lineHeight = '1.2';
meta.style.overflow = 'hidden';
meta.style.textOverflow = 'ellipsis';
@ -529,7 +639,7 @@ class EndScreenOverlay extends Component {
return info;
}
createSwiperIndicators(totalVideos, swiperWrapper) {
createSwiperIndicators(totalVideos, swiperWrapper, itemsPerView = 2) {
const indicators = videojs.dom.createEl('div', {
className: 'vjs-swiper-indicators',
});
@ -539,7 +649,6 @@ class EndScreenOverlay extends Component {
indicators.style.gap = '8px';
indicators.style.marginTop = '10px';
const itemsPerView = 2;
const totalPages = Math.ceil(totalVideos / itemsPerView);
for (let i = 0; i < totalPages; i++) {

View File

@ -1,194 +0,0 @@
/* ===== EMBED PLAYER STYLES ===== */
/* Styles specific to #page-embed and embedded video players */
/* Fullscreen video styles for embedded video player */
#page-embed .video-js-root-embed .video-js video {
width: 100vw !important;
height: 100vh !important;
object-fit: cover !important;
border-radius: 0 !important;
}
#page-embed .video-js-root-embed .video-js .vjs-poster {
border-radius: 0 !important;
width: 100vw !important;
height: 100vh !important;
object-fit: cover !important;
}
/* Fullscreen styles for embedded video player */
#page-embed .video-js-root-embed .video-container {
width: 100vw;
height: 100vh;
max-width: none;
margin: 0;
padding: 0;
box-sizing: border-box;
position: fixed;
top: 0;
left: 0;
z-index: 1000;
}
/* Fullscreen fluid styles for embedded video player */
#page-embed .video-js-root-embed .video-js.vjs-fluid {
width: 100vw !important;
height: 100vh !important;
max-width: none !important;
max-height: none !important;
}
/* Fullscreen video-js player styles for embedded video player */
#page-embed .video-js-root-embed .video-js {
width: 100vw !important;
height: 100vh !important;
border-radius: 0;
position: relative;
overflow: hidden; /* Prevent scrollbars in embed video player */
}
/* Prevent page scrolling when embed is active */
#page-embed .video-js-root-embed {
position: fixed;
top: 0;
overflow: hidden; /* Prevent scrollbars in embed mode */
}
/* Sticky controls for embed player - always at bottom of window */
#page-embed .video-js-root-embed .video-js .vjs-control-bar {
position: fixed !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
width: 100vw !important;
z-index: 1001 !important;
background: transparent !important;
background-color: transparent !important;
background-image: none !important;
padding: 0 12px !important;
margin: 0 !important;
border: none !important;
box-shadow: none !important;
}
/* Ensure progress bar is also sticky for embed player */
#page-embed .video-js-root-embed .video-js .vjs-progress-control {
position: fixed !important;
bottom: 48px !important;
left: 0 !important;
right: 0 !important;
width: 100vw !important;
z-index: 1000 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
/* Ensure gradient overlay extends to full window width for embed */
#page-embed .video-js-root-embed .video-js::after {
position: fixed !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
width: 100vw !important;
height: 120px !important;
z-index: 999 !important;
}
/* Mobile optimizations for embed player sticky controls */
@media (max-width: 768px) {
#page-embed .video-js-root-embed .video-js .vjs-control-bar {
height: 56px !important; /* Larger touch target on mobile */
padding: 0 16px !important; /* More padding for touch */
margin: 0 !important;
border: none !important;
background: transparent !important;
background-color: transparent !important;
background-image: none !important;
}
#page-embed .video-js-root-embed .video-js .vjs-progress-control {
bottom: 44px !important; /* Much closer to control bar - minimal gap */
margin: 0 !important;
padding: 0 !important;
}
/* Ensure controls don't interfere with mobile browser chrome */
#page-embed .video-js-root-embed .video-js .vjs-control-bar {
padding-bottom: env(safe-area-inset-bottom, 0) !important;
}
}
/* Ensure controls are always visible when user is active (embed only) */
#page-embed .video-js-root-embed .video-js.vjs-user-active .vjs-control-bar,
#page-embed .video-js-root-embed .video-js.vjs-paused .vjs-control-bar,
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-control-bar {
opacity: 1 !important;
visibility: visible !important;
transform: translateY(0) !important;
}
/* Smooth transitions for control visibility */
#page-embed .video-js-root-embed .video-js .vjs-control-bar {
transition:
opacity 0.3s ease,
transform 0.3s ease !important;
}
/* Hide controls when user is inactive (but keep them sticky) */
#page-embed .video-js-root-embed .video-js.vjs-user-inactive:not(.vjs-paused):not(.vjs-ended) .vjs-control-bar {
opacity: 0 !important;
transform: translateY(100%) !important;
}
#page-embed .video-js-root-embed .video-js.vjs-user-inactive:not(.vjs-paused):not(.vjs-ended) .vjs-progress-control {
opacity: 0 !important;
}
/* ===== EMBED-SPECIFIC SEEK INDICATOR POSITIONING ===== */
/* Ensure play icon (SeekIndicator) stays centered in embed view regardless of window size */
#page-embed .video-js-root-embed .video-js .vjs-seek-indicator {
position: fixed !important;
top: 50vh !important;
left: 50vw !important;
transform: translate(-50%, -50%) !important;
z-index: 10000 !important;
pointer-events: none !important;
display: none !important;
align-items: center !important;
justify-content: center !important;
opacity: 0 !important;
visibility: hidden !important;
transition: opacity 0.2s ease-in-out !important;
width: auto !important;
height: auto !important;
margin: 0 !important;
padding: 0 !important;
}
/* ===== EMBED-SPECIFIC BIG PLAY BUTTON POSITIONING ===== */
/* Ensure big play button stays centered in embed view regardless of window size */
#page-embed .video-js-root-embed .video-js .vjs-big-play-button {
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
z-index: 1000 !important;
pointer-events: auto !important;
margin: 0 !important;
padding: 0 !important;
}
/* ===== EMBED-SPECIFIC CONTROLS HIDING FOR INITIAL STATE ===== */
/* Hide seekbar and controls when poster is displayed (before first play) in embed mode */
#page-embed .video-js-root-embed .video-js:not(.vjs-has-started) .vjs-control-bar {
display: none !important;
opacity: 0 !important;
visibility: hidden !important;
}
#page-embed .video-js-root-embed .video-js:not(.vjs-has-started) .vjs-progress-control {
display: none !important;
opacity: 0 !important;
visibility: hidden !important;
}

View File

@ -276,7 +276,7 @@ export const BulkActionPermissionModal: React.FC<BulkActionPermissionModalProps>
<div className="permission-panel">
<h3>
{translateString('To add')}
{permissionType === 'viewer' ? translateString('Existing co-viewers') : permissionType === 'editor' ? translateString('Existing co-editors') : translateString('Existing co-owners')}
{selectedMediaIds.length > 1 && (
<span className="info-tooltip" title={translateString('The intersection of users in the selected media is shown')}>
?

View File

@ -16,6 +16,7 @@ import React, { useEffect, useRef } from 'react';
const VideoJSEmbed = ({
data,
useRoundedCorners,
version,
isPlayList,
playerVolume,
playerSoundMuted,
@ -67,6 +68,7 @@ const VideoJSEmbed = ({
window.MEDIA_DATA = {
data: data || {},
useRoundedCorners: useRoundedCorners,
version: version,
isPlayList: isPlayList,
playerVolume: playerVolume || 0.5,
playerSoundMuted: playerSoundMuted || (urlMuted === '1'),
@ -204,14 +206,14 @@ const VideoJSEmbed = ({
if (!existingCSS) {
const cssLink = document.createElement('link');
cssLink.rel = 'stylesheet';
cssLink.href = siteUrl + '/static/video_js/video-js.css';
cssLink.href = siteUrl + '/static/video_js/video-js.css?v=' + version;
document.head.appendChild(cssLink);
}
// Load JS if not already loaded
if (!existingJS) {
const script = document.createElement('script');
script.src = siteUrl + '/static/video_js/video-js.js';
script.src = siteUrl + '/static/video_js/video-js.js?v=' + version;
document.head.appendChild(script);
}
};

View File

@ -49,6 +49,11 @@ export function UserItemMemberSince(props) {
}
export function TaxonomyItemMediaCount(props) {
// Check if listing numbers should be included based on settings
if (!window.MediaCMS.features.listings.includeNumbers) {
return null;
}
return (
<span key="item-media-count" className="item-media-count">
{' ' + props.count} media

View File

@ -392,6 +392,7 @@ export default class VideoViewer extends React.PureComponent {
return React.createElement(VideoJSEmbed, {
data: this.props.data,
useRoundedCorners: site.useRoundedCorners,
version: site.version,
isPlayList: !!MediaPageStore.get('playlist-id'),
playerVolume: this.browserCache.get('player-volume'),
playerSoundMuted: this.browserCache.get('player-sound-muted'),

View File

@ -1,15 +0,0 @@
import React from 'react';
import { SiteConsumer } from '../utils/contexts/';
import VideoViewer from '../components/media-viewer/VideoViewer';
import { _VideoMediaPage } from './_VideoMediaPage';
export class MediaVideoPage extends _VideoMediaPage {
viewerContainerContent(mediaData) {
return <>Not working anymore?</>; // TODO: check this if this page not working anymore as MediaPage.js do the same work
return <SiteConsumer>{(site) => <VideoViewer data={mediaData} siteUrl={site.url} inEmbed={!1} />}</SiteConsumer>;
}
mediaType() {
return 'video';
}
}

View File

@ -592,6 +592,8 @@ export class ProfileMediaPage extends Page {
this.onFiltersUpdate({
media_type: this.state.filterArgs.includes('media_type') ? this.state.filterArgs.match(/media_type=([^&]*)/)?.[1] : null,
upload_date: this.state.filterArgs.includes('upload_date') ? this.state.filterArgs.match(/upload_date=([^&]*)/)?.[1] : null,
duration: this.state.filterArgs.includes('duration') ? this.state.filterArgs.match(/duration=([^&]*)/)?.[1] : null,
publish_state: this.state.filterArgs.includes('publish_state') ? this.state.filterArgs.match(/publish_state=([^&]*)/)?.[1] : null,
sort_by: this.state.selectedSort,
tag: tag,
});
@ -604,6 +606,8 @@ export class ProfileMediaPage extends Page {
this.onFiltersUpdate({
media_type: this.state.filterArgs.includes('media_type') ? this.state.filterArgs.match(/media_type=([^&]*)/)?.[1] : null,
upload_date: this.state.filterArgs.includes('upload_date') ? this.state.filterArgs.match(/upload_date=([^&]*)/)?.[1] : null,
duration: this.state.filterArgs.includes('duration') ? this.state.filterArgs.match(/duration=([^&]*)/)?.[1] : null,
publish_state: this.state.filterArgs.includes('publish_state') ? this.state.filterArgs.match(/publish_state=([^&]*)/)?.[1] : null,
sort_by: sortOption,
tag: this.state.selectedTag,
});

View File

@ -179,6 +179,8 @@ class ProfileSharedByMePage extends Page {
this.onFiltersUpdate({
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: this.state.selectedSort,
tag: tag,
});
@ -190,6 +192,8 @@ class ProfileSharedByMePage extends Page {
this.onFiltersUpdate({
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: sortBy,
tag: this.state.selectedTag,
});
@ -200,6 +204,8 @@ class ProfileSharedByMePage extends Page {
const args = {
media_type: null,
upload_date: null,
duration: null,
publish_state: null,
sort_by: null,
ordering: null,
t: null,
@ -223,6 +229,16 @@ class ProfileSharedByMePage extends Page {
break;
}
// Handle duration filter
if (updatedArgs.duration && updatedArgs.duration !== 'all') {
args.duration = updatedArgs.duration;
}
// Handle publish state filter
if (updatedArgs.publish_state && updatedArgs.publish_state !== 'all') {
args.publish_state = updatedArgs.publish_state;
}
switch (updatedArgs.sort_by) {
case 'date_added_desc':
// Default sorting, no need to add parameters
@ -301,7 +317,9 @@ class ProfileSharedByMePage extends Page {
// Check if any filters are active
const hasActiveFilters = this.state.filterArgs && (
this.state.filterArgs.includes('media_type=') ||
this.state.filterArgs.includes('upload_date=')
this.state.filterArgs.includes('upload_date=') ||
this.state.filterArgs.includes('duration=') ||
this.state.filterArgs.includes('publish_state=')
);
return [
@ -341,7 +359,7 @@ class ProfileSharedByMePage extends Page {
itemsCountCallback={this.state.requestUrl ? this.getCountFunc : null}
hideViews={!PageStore.get('config-media-item').displayViews}
hideDate={!PageStore.get('config-media-item').displayPublishDate}
canEdit={false}
canEdit={isMediaAuthor}
onResponseDataLoaded={this.onResponseDataLoaded}
showSelection={isMediaAuthor}
hasAnySelection={this.props.bulkActions.selectedMedia.size > 0}

View File

@ -177,6 +177,8 @@ export class ProfileSharedWithMePage extends Page {
this.onFiltersUpdate({
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: this.state.selectedSort,
tag: tag,
});
@ -188,6 +190,8 @@ export class ProfileSharedWithMePage extends Page {
this.onFiltersUpdate({
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: sortBy,
tag: this.state.selectedTag,
});
@ -198,6 +202,8 @@ export class ProfileSharedWithMePage extends Page {
const args = {
media_type: null,
upload_date: null,
duration: null,
publish_state: null,
sort_by: null,
ordering: null,
t: null,
@ -221,6 +227,16 @@ export class ProfileSharedWithMePage extends Page {
break;
}
// Handle duration filter
if (updatedArgs.duration && updatedArgs.duration !== 'all') {
args.duration = updatedArgs.duration;
}
// Handle publish state filter
if (updatedArgs.publish_state && updatedArgs.publish_state !== 'all') {
args.publish_state = updatedArgs.publish_state;
}
switch (updatedArgs.sort_by) {
case 'date_added_desc':
// Default sorting, no need to add parameters
@ -299,7 +315,9 @@ export class ProfileSharedWithMePage extends Page {
// Check if any filters are active
const hasActiveFilters = this.state.filterArgs && (
this.state.filterArgs.includes('media_type=') ||
this.state.filterArgs.includes('upload_date=')
this.state.filterArgs.includes('upload_date=') ||
this.state.filterArgs.includes('duration=') ||
this.state.filterArgs.includes('publish_state=')
);
return [

View File

@ -7,6 +7,7 @@ export function init(settings) {
api: '',
title: '',
useRoundedCorners: true,
version: '1.0.0',
};
if (void 0 !== settings) {
@ -29,6 +30,10 @@ export function init(settings) {
if ('boolean' === typeof settings.useRoundedCorners) {
SITE.useRoundedCorners = settings.useRoundedCorners;
}
if ('string' === typeof settings.version) {
SITE.version = settings.version.trim();
}
}
}

View File

@ -5,6 +5,7 @@ module.exports = {
url: process.env.MEDIACMS_URL || 'UNDEFINED_URL',
api: process.env.MEDIACMS_API || 'UNDEFINED_API',
useRoundedCorners: true,
version: '1.0.0',
theme: {
mode: 'light', // Valid values: 'light', 'dark'.
switch: {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,9 @@
{% block headtitle %}Add subtitle - {{PORTAL_NAME}}{% endblock headtitle %}
{% block innercontent %}
{% include "cms/media_nav.html" with active_tab="subtitles" %}
<div class="user-action-form-wrap">
{% include "cms/media_nav.html" with active_tab="subtitles" %}
</div>
{% if whisper_form %}
<div class="user-action-form-wrap">

View File

@ -5,10 +5,9 @@
{% block headtitle %}Edit video chapters - {{PORTAL_NAME}}{% endblock headtitle %}
{% block topimports %}
<link href="{% static "chapters_editor/chapters-editor.css" %}" rel="preload" as="style">
<link href="{% static "chapters_editor/chapters-editor.css" %}" rel="stylesheet">
<script src="{% static 'chapters_editor/chapters-editor.js' %}"></script>
<link href="{% static 'chapters_editor/chapters-editor.css' %}?v={{ VERSION }}" rel="preload" as="style">
<link href="{% static 'chapters_editor/chapters-editor.css' %}?v={{ VERSION }}" rel="stylesheet">
<script src="{% static 'chapters_editor/chapters-editor.js' %}?v={{ VERSION }}"></script>
<script>
window.MEDIA_DATA = {

View File

@ -5,10 +5,9 @@
{% block headtitle %}Edit video - {{PORTAL_NAME}}{% endblock headtitle %}
{% block topimports %}
<link href="{% static "video_editor/video-editor.css" %}" rel="preload" as="style">
<link href="{% static "video_editor/video-editor.css" %}" rel="stylesheet">
<script src="{% static 'video_editor/video-editor.js' %}"></script>
<link href="{% static 'video_editor/video-editor.css' %}?v={{ VERSION }}" rel="preload" as="style">
<link href="{% static 'video_editor/video-editor.css' %}?v={{ VERSION }}" rel="stylesheet">
<script src="{% static 'video_editor/video-editor.js' %}?v={{ VERSION }}"></script>
<script>
window.MEDIA_DATA = {

View File

@ -33,6 +33,9 @@ MediaCMS.features = {
hideViews: false,
hideAuthor: false,
},
listings:{
includeNumbers: {% if INCLUDE_LISTING_NUMBERS %}true{% else %}false{% endif %},
},
playlists:{
mediaTypes: ['audio', 'video'],
}

View File

@ -4,6 +4,7 @@ MediaCMS.site = {
url: '{{FRONTEND_HOST}}',
api: '{{FRONTEND_HOST}}/api/v1',
useRoundedCorners: {% if USE_ROUNDED_CORNERS %}true{% else %}false{% endif %},
version: '{{VERSION}}',
theme: {
mode: '{{DEFAULT_THEME}}',
switch: {