Compare commits

...

5 Commits

Author SHA1 Message Date
Markos Gogoulos
ba2c31b1e6 fix: static files (#1429) 2025-11-12 14:08:02 +02:00
Yiannis Christodoulou
5eb6fafb8c fix: Show default chapter names in textarea instead of placeholder text (#1428)
* Refactor chapter filtering and auto-save logic

Simplified chapter filtering to only exclude empty titles, allowing default chapter names. Updated auto-save logic to skip saving when there are no chapters or mediaId. Removed unused helper function and improved debug logging.

* Show default chapter title in editor and set initial title

The chapter title is now always displayed in the textarea, including default names like 'Chapter 1'. Also, the initial segment is created with 'Chapter 1' as its title instead of an empty string for better clarity.

* build assets
2025-11-12 14:04:07 +02:00
Markos Gogoulos
c035bcddf5 small 7.2.x fixes 2025-11-11 19:51:42 +02:00
Markos Gogoulos
01912ea1f9 fix: adjust poster url for audio 2025-11-11 13:21:10 +02:00
Markos Gogoulos
d9f299af4d V7 small fixes (#1426) 2025-11-11 13:15:36 +02:00
13 changed files with 94 additions and 94 deletions

View File

@@ -1 +1 @@
VERSION = "7.2.0" VERSION = "7.2.1"

View File

@@ -178,14 +178,11 @@ class MediaPublishForm(forms.ModelForm):
state = cleaned_data.get("state") state = cleaned_data.get("state")
categories = cleaned_data.get("category") categories = cleaned_data.get("category")
if getattr(settings, 'USE_RBAC', False) and 'category' in self.fields: if state in ['private', 'unlisted']:
custom_permissions = self.instance.permissions.exists()
rbac_categories = categories.filter(is_rbac_category=True).values_list('title', flat=True) rbac_categories = categories.filter(is_rbac_category=True).values_list('title', flat=True)
if rbac_categories or custom_permissions:
if rbac_categories and state in ['private', 'unlisted']:
# Make the confirm_state field visible and add it to the layout
self.fields['confirm_state'].widget = forms.CheckboxInput() self.fields['confirm_state'].widget = forms.CheckboxInput()
# add it after the state field
state_index = None state_index = None
for i, layout_item in enumerate(self.helper.layout): for i, layout_item in enumerate(self.helper.layout):
if isinstance(layout_item, CustomField) and layout_item.fields[0] == 'state': if isinstance(layout_item, CustomField) and layout_item.fields[0] == 'state':
@@ -198,8 +195,12 @@ class MediaPublishForm(forms.ModelForm):
self.helper.layout = Layout(*layout_items) self.helper.layout = Layout(*layout_items)
if not cleaned_data.get('confirm_state'): if not cleaned_data.get('confirm_state'):
error_message = f"I understand that although media state is {state}, the media is also shared with users that have access to the following categories: {', '.join(rbac_categories)}" if rbac_categories:
self.add_error('confirm_state', error_message) error_message = f"I understand that although media state is {state}, the media is also shared with users that have access to categories: {', '.join(rbac_categories)}"
self.add_error('confirm_state', error_message)
if custom_permissions:
error_message = f"I understand that although media state is {state}, the media is also shared by me with other users, that I can see in the 'Shared by me' page"
self.add_error('confirm_state', error_message)
return cleaned_data return cleaned_data

View File

@@ -763,6 +763,8 @@ class Media(models.Model):
return helpers.url_from_path(self.uploaded_thumbnail.path) return helpers.url_from_path(self.uploaded_thumbnail.path)
if self.thumbnail: if self.thumbnail:
return helpers.url_from_path(self.thumbnail.path) return helpers.url_from_path(self.thumbnail.path)
if self.media_type == "audio":
return helpers.url_from_path("userlogos/poster_audio.jpg")
return None return None
@property @property
@@ -776,6 +778,9 @@ class Media(models.Model):
return helpers.url_from_path(self.uploaded_poster.path) return helpers.url_from_path(self.uploaded_poster.path)
if self.poster: if self.poster:
return helpers.url_from_path(self.poster.path) return helpers.url_from_path(self.poster.path)
if self.media_type == "audio":
return helpers.url_from_path("userlogos/poster_audio.jpg")
return None return None
@property @property

View File

@@ -26,18 +26,6 @@ const mediaPageLinkStyles = {
}, },
} as const; } as const;
// Helper function to parse time string (HH:MM:SS.mmm) to seconds
const parseTimeToSeconds = (timeString: string): number => {
const parts = timeString.split(':');
if (parts.length !== 3) return 0;
const hours = parseInt(parts[0], 10) || 0;
const minutes = parseInt(parts[1], 10) || 0;
const seconds = parseFloat(parts[2]) || 0;
return hours * 3600 + minutes * 60 + seconds;
};
interface TimelineControlsProps { interface TimelineControlsProps {
currentTime: number; currentTime: number;
duration: number; duration: number;
@@ -203,17 +191,7 @@ const TimelineControls = ({
setIsAutoSaving(true); setIsAutoSaving(true);
// Format segments data for API request - use ref to get latest segments and sort by start time // Format segments data for API request - use ref to get latest segments and sort by start time
// ONLY save chapters that have custom titles - filter out chapters without titles or with default names
const chapters = clipSegmentsRef.current const chapters = clipSegmentsRef.current
.filter((segment) => {
// Filter out empty titles
if (!segment.chapterTitle || !segment.chapterTitle.trim()) {
return false;
}
// Filter out default chapter names like "Chapter 1", "Chapter 2", etc.
const isDefaultName = /^Chapter \d+$/.test(segment.chapterTitle);
return !isDefaultName;
})
.sort((a, b) => a.startTime - b.startTime) // Sort by start time chronologically .sort((a, b) => a.startTime - b.startTime) // Sort by start time chronologically
.map((chapter) => ({ .map((chapter) => ({
startTime: formatDetailedTime(chapter.startTime), startTime: formatDetailedTime(chapter.startTime),
@@ -221,7 +199,7 @@ const TimelineControls = ({
chapterTitle: chapter.chapterTitle, chapterTitle: chapter.chapterTitle,
})); }));
logger.debug('Filtered chapters (only custom titles):', chapters); logger.debug('chapters', chapters);
const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null; const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null;
// For testing, use '1234' if no mediaId is available // For testing, use '1234' if no mediaId is available
@@ -229,13 +207,12 @@ const TimelineControls = ({
logger.debug('mediaId', finalMediaId); logger.debug('mediaId', finalMediaId);
if (!finalMediaId) { if (!finalMediaId || chapters.length === 0) {
logger.debug('No mediaId, skipping auto-save'); logger.debug('No mediaId or segments, skipping auto-save');
setIsAutoSaving(false); setIsAutoSaving(false);
return; return;
} }
// Save chapters (empty array if no chapters have titles)
logger.debug('Auto-saving segments:', { mediaId: finalMediaId, chapters }); logger.debug('Auto-saving segments:', { mediaId: finalMediaId, chapters });
const response = await autoSaveVideo(finalMediaId, { chapters }); const response = await autoSaveVideo(finalMediaId, { chapters });
@@ -291,13 +268,8 @@ const TimelineControls = ({
// Update editing title when selected segment changes // Update editing title when selected segment changes
useEffect(() => { useEffect(() => {
if (selectedSegment) { if (selectedSegment) {
// Check if the chapter title is a default generated name (e.g., "Chapter 1", "Chapter 2", etc.) // Always show the chapter title in the textarea, whether it's default or custom
const isDefaultChapterName = selectedSegment.chapterTitle && setEditingChapterTitle(selectedSegment.chapterTitle || '');
/^Chapter \d+$/.test(selectedSegment.chapterTitle);
// If it's a default name, show empty string so placeholder appears
// If it's a custom title, show the actual title
setEditingChapterTitle(isDefaultChapterName ? '' : (selectedSegment.chapterTitle || ''));
} else { } else {
setEditingChapterTitle(''); setEditingChapterTitle('');
} }
@@ -522,20 +494,11 @@ const TimelineControls = ({
try { try {
// Format chapters data for API request - sort by start time first // Format chapters data for API request - sort by start time first
// ONLY save chapters that have custom titles - filter out chapters without titles or with default names
const chapters = clipSegments const chapters = clipSegments
.filter((segment) => { .filter((segment) => segment.chapterTitle && segment.chapterTitle.trim())
// Filter out empty titles
if (!segment.chapterTitle || !segment.chapterTitle.trim()) {
return false;
}
// Filter out default chapter names like "Chapter 1", "Chapter 2", etc.
const isDefaultName = /^Chapter \d+$/.test(segment.chapterTitle);
return !isDefaultName;
})
.sort((a, b) => a.startTime - b.startTime) // Sort by start time chronologically .sort((a, b) => a.startTime - b.startTime) // Sort by start time chronologically
.map((segment) => ({ .map((segment) => ({
chapterTitle: segment.chapterTitle, chapterTitle: segment.chapterTitle || `Chapter ${segment.id}`,
from: formatDetailedTime(segment.startTime), from: formatDetailedTime(segment.startTime),
to: formatDetailedTime(segment.endTime), to: formatDetailedTime(segment.endTime),
})); }));
@@ -4119,4 +4082,4 @@ const TimelineControls = ({
); );
}; };
export default TimelineControls; export default TimelineControls;

View File

@@ -150,7 +150,7 @@ const useVideoChapters = () => {
// Create a default segment that spans the entire video on first load // Create a default segment that spans the entire video on first load
const initialSegment: Segment = { const initialSegment: Segment = {
id: 1, id: 1,
chapterTitle: '', chapterTitle: 'Chapter 1',
startTime: 0, startTime: 0,
endTime: video.duration, endTime: video.duration,
}; };

View File

@@ -21,12 +21,16 @@ function downloadOptionsList() {
for (g in encodings_info[k]) { for (g in encodings_info[k]) {
if (encodings_info[k].hasOwnProperty(g)) { if (encodings_info[k].hasOwnProperty(g)) {
if ('success' === encodings_info[k][g].status && 100 === encodings_info[k][g].progress && null !== encodings_info[k][g].url) { if ('success' === encodings_info[k][g].status && 100 === encodings_info[k][g].progress && null !== encodings_info[k][g].url) {
// Use original media URL for download instead of encoded version
const originalUrl = media_data.original_media_url;
const originalFilename = originalUrl ? originalUrl.substring(originalUrl.lastIndexOf('/') + 1) : media_data.title;
optionsList[encodings_info[k][g].title] = { optionsList[encodings_info[k][g].title] = {
text: k + ' - ' + g.toUpperCase() + ' (' + encodings_info[k][g].size + ')', text: k + ' - ' + g.toUpperCase() + ' (' + encodings_info[k][g].size + ')',
link: formatInnerLink(encodings_info[k][g].url, SiteContext._currentValue.url), link: formatInnerLink(media_data.original_media_url, SiteContext._currentValue.url),
linkAttr: { linkAttr: {
target: '_blank', target: '_blank',
download: media_data.title + '_' + k + '_' + g.toUpperCase(), download: originalFilename,
}, },
}; };
} }
@@ -36,12 +40,16 @@ function downloadOptionsList() {
} }
} }
// Extract actual filename from the original media URL
const originalUrl = media_data.original_media_url;
const originalFilename = originalUrl ? originalUrl.substring(originalUrl.lastIndexOf('/') + 1) : media_data.title;
optionsList.original_media_url = { optionsList.original_media_url = {
text: 'Original file (' + media_data.size + ')', text: 'Original file (' + media_data.size + ')',
link: formatInnerLink(media_data.original_media_url, SiteContext._currentValue.url), link: formatInnerLink(media_data.original_media_url, SiteContext._currentValue.url),
linkAttr: { linkAttr: {
target: '_blank', target: '_blank',
download: media_data.title, download: originalFilename,
}, },
}; };

View File

@@ -54,6 +54,10 @@ export default class ViewerInfoTitleBanner extends React.PureComponent {
? formatInnerLink(MediaPageStore.get('media-original-url'), SiteContext._currentValue.url) ? formatInnerLink(MediaPageStore.get('media-original-url'), SiteContext._currentValue.url)
: null; : null;
// Extract actual filename from URL for non-video downloads
const originalUrl = MediaPageStore.get('media-original-url');
this.downloadFilename = originalUrl ? originalUrl.substring(originalUrl.lastIndexOf('/') + 1) : this.props.title;
this.updateStateValues = this.updateStateValues.bind(this); this.updateStateValues = this.updateStateValues.bind(this);
} }
@@ -171,7 +175,7 @@ export default class ViewerInfoTitleBanner extends React.PureComponent {
.downloadLink ? ( .downloadLink ? (
<VideoMediaDownloadLink /> <VideoMediaDownloadLink />
) : ( ) : (
<OtherMediaDownloadLink link={this.downloadLink} title={this.props.title} /> <OtherMediaDownloadLink link={this.downloadLink} title={this.downloadFilename} />
)} )}
<MediaMoreOptionsIcon allowDownload={this.props.allowDownload} /> <MediaMoreOptionsIcon allowDownload={this.props.allowDownload} />

View File

@@ -77,7 +77,7 @@ export default class ViewerInfoVideoTitleBanner extends ViewerInfoTitleBanner {
.downloadLink ? ( .downloadLink ? (
<VideoMediaDownloadLink /> <VideoMediaDownloadLink />
) : ( ) : (
<OtherMediaDownloadLink link={this.downloadLink} title={this.props.title} /> <OtherMediaDownloadLink link={this.downloadLink} title={this.downloadFilename} />
)} )}
<MediaMoreOptionsIcon allowDownload={this.props.allowDownload} /> <MediaMoreOptionsIcon allowDownload={this.props.allowDownload} />

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { useBulkActions } from '../hooks/useBulkActions';
/**
* Higher-Order Component that provides bulk actions functionality
* to class components via props
*/
export function withBulkActions(WrappedComponent) {
return function WithBulkActionsComponent(props) {
const bulkActions = useBulkActions();
return (
<WrappedComponent
{...props}
bulkActions={bulkActions}
/>
);
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

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