Compare commits

..

No commits in common. "930b80079e077ae2444f493f199cb9b9070f5e6b" and "1f4ed59127970236f48913d13450631af4ec2ca7" have entirely different histories.

34 changed files with 418 additions and 658 deletions

View File

@ -1 +1 @@
VERSION = "6.9.103" VERSION = "6.7.103"

View File

@ -205,11 +205,7 @@ class SubtitleAdmin(admin.ModelAdmin):
class VideoTrimRequestAdmin(admin.ModelAdmin): class VideoTrimRequestAdmin(admin.ModelAdmin):
list_display = ["media", "status", "add_date", "video_action", "media_trim_style", "timestamps"] pass
list_filter = ["status", "video_action", "media_trim_style", "add_date"]
search_fields = ["media__title"]
readonly_fields = ("add_date",)
ordering = ("-add_date",)
class EncodingAdmin(admin.ModelAdmin): class EncodingAdmin(admin.ModelAdmin):
@ -228,11 +224,7 @@ class EncodingAdmin(admin.ModelAdmin):
class TranscriptionRequestAdmin(admin.ModelAdmin): class TranscriptionRequestAdmin(admin.ModelAdmin):
list_display = ["media", "add_date", "status", "translate_to_english"] pass
list_filter = ["status", "translate_to_english", "add_date"]
search_fields = ["media__title"]
readonly_fields = ("add_date", "logs")
ordering = ("-add_date",)
class PageAdminForm(forms.ModelForm): class PageAdminForm(forms.ModelForm):

View File

@ -80,9 +80,5 @@ class TranscriptionRequest(models.Model):
translate_to_english = models.BooleanField(default=False) translate_to_english = models.BooleanField(default=False)
logs = models.TextField(blank=True, null=True) logs = models.TextField(blank=True, null=True)
class Meta:
verbose_name = "Caption Request"
verbose_name_plural = "Caption Requests"
def __str__(self): def __str__(self):
return f"Transcription request for {self.media.title} - {self.status}" return f"Transcription request for {self.media.title} - {self.status}"

View File

@ -77,10 +77,6 @@ class VideoTrimRequest(models.Model):
media_trim_style = models.CharField(max_length=20, choices=TRIM_STYLE_CHOICES, default="no_encoding") media_trim_style = models.CharField(max_length=20, choices=TRIM_STYLE_CHOICES, default="no_encoding")
timestamps = models.JSONField(null=False, blank=False, help_text="Timestamps for trimming") timestamps = models.JSONField(null=False, blank=False, help_text="Timestamps for trimming")
class Meta:
verbose_name = "Trim Request"
verbose_name_plural = "Trim Requests"
def __str__(self): def __str__(self):
return f"Trim request for {self.media.title} ({self.status})" return f"Trim request for {self.media.title} ({self.status})"

View File

@ -192,9 +192,6 @@ class MediaList(APIView):
media = self._get_media_queryset(request, user) media = self._get_media_queryset(request, user)
already_sorted = True already_sorted = True
else:
if is_mediacms_editor(self.request.user):
media = Media.objects.prefetch_related("user", "tags")
else: else:
media = self._get_media_queryset(request) media = self._get_media_queryset(request)
already_sorted = True already_sorted = True
@ -1013,11 +1010,7 @@ class MediaSearch(APIView):
return Response(ret, status=status.HTTP_200_OK) return Response(ret, status=status.HTTP_200_OK)
if request.user.is_authenticated: if request.user.is_authenticated:
if is_mediacms_editor(self.request.user): basic_query = Q(listable=True) | Q(permissions__user=request.user)
media = Media.objects.prefetch_related("user", "tags")
basic_query = Q()
else:
basic_query = Q(listable=True) | Q(permissions__user=request.user) | Q(user=request.user)
if getattr(settings, 'USE_RBAC', False): if getattr(settings, 'USE_RBAC', False):
rbac_categories = request.user.get_rbac_categories_as_member() rbac_categories = request.user.get_rbac_categories_as_member()

View File

@ -170,16 +170,15 @@
} }
} }
.category-modal { .available-categories {
.available-categories {
margin-top: 16px; margin-top: 16px;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: visible; overflow: visible;
} }
.category-list { .category-list {
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
padding: 8px; padding: 8px;
@ -225,9 +224,9 @@
} }
} }
} }
} }
.category-item { .category-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -279,10 +278,10 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
} }
.add-btn, .add-btn,
.remove-btn { .remove-btn {
background: none; background: none;
border: none; border: none;
font-size: 24px; font-size: 24px;
@ -297,9 +296,9 @@
border-radius: 4px; border-radius: 4px;
transition: all 0.2s ease; transition: all 0.2s ease;
flex-shrink: 0; flex-shrink: 0;
} }
.add-btn { .add-btn {
color: var(--default-theme-color, #009933); color: var(--default-theme-color, #009933);
&:hover { &:hover {
@ -313,9 +312,9 @@
background-color: rgba(102, 187, 102, 0.2); background-color: rgba(102, 187, 102, 0.2);
} }
} }
} }
.remove-btn { .remove-btn {
color: #dc3545; color: #dc3545;
&:hover { &:hover {
@ -331,10 +330,10 @@
color: #ff8787; color: #ff8787;
} }
} }
} }
.empty-message, .empty-message,
.loading-message { .loading-message {
padding: 40px 20px; padding: 40px 20px;
text-align: center; text-align: center;
color: #999; color: #999;
@ -344,7 +343,6 @@
.dark_theme & { .dark_theme & {
color: #666; color: #666;
} }
}
} }
.category-modal-footer { .category-modal-footer {

View File

@ -144,17 +144,16 @@
} }
.search-results { .search-results {
position: absolute; position: fixed;
top: 100%; left: auto;
left: 0; right: auto;
right: 0;
background-color: #f9f9f9; background-color: #f9f9f9;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
margin-top: 4px; margin-top: 4px;
max-height: 250px; max-height: 250px;
overflow-y: auto; overflow-y: auto;
z-index: 1000; z-index: 10001;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
.dark_theme & { .dark_theme & {

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import './BulkActionChangeOwnerModal.scss'; import './BulkActionChangeOwnerModal.scss';
import { translateString } from '../utils/helpers/'; import { translateString } from '../utils/helpers/';
@ -29,6 +29,8 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
const [selectedUser, setSelectedUser] = useState<User | null>(null); const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null); const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
const searchBoxRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
@ -39,6 +41,17 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
} }
}, [isOpen]); }, [isOpen]);
const updateDropdownPosition = () => {
if (searchBoxRef.current) {
const rect = searchBoxRef.current.getBoundingClientRect();
setDropdownPosition({
top: rect.bottom,
left: rect.left,
width: rect.width,
});
}
};
const searchUsers = async (name: string) => { const searchUsers = async (name: string) => {
if (!name.trim()) { if (!name.trim()) {
setSearchResults([]); setSearchResults([]);
@ -53,6 +66,7 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
const data = await response.json(); const data = await response.json();
setSearchResults(data.results || data); setSearchResults(data.results || data);
updateDropdownPosition();
} catch (error) { } catch (error) {
console.error('Error searching users:', error); console.error('Error searching users:', error);
} }
@ -130,7 +144,7 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
</div> </div>
<div className="change-owner-modal-content"> <div className="change-owner-modal-content">
<div className="search-box-wrapper"> <div className="search-box-wrapper" ref={searchBoxRef}>
<div className="search-box"> <div className="search-box">
<input <input
type="text" type="text"
@ -139,19 +153,6 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
onChange={(e) => handleSearchChange(e.target.value)} onChange={(e) => handleSearchChange(e.target.value)}
/> />
</div> </div>
{searchResults.length > 0 && (
<div className="search-results">
{searchResults.slice(0, 10).map((user) => (
<div
key={user.username}
className="search-result-item"
onClick={() => handleUserSelect(user)}
>
{user.name} - {user.username}
</div>
))}
</div>
)}
</div> </div>
{selectedUser && ( {selectedUser && (
@ -174,6 +175,27 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
</button> </button>
</div> </div>
</div> </div>
{searchResults.length > 0 && (
<div
className="search-results"
style={{
top: `${dropdownPosition.top}px`,
left: `${dropdownPosition.left}px`,
width: `${dropdownPosition.width}px`,
}}
>
{searchResults.slice(0, 10).map((user) => (
<div
key={user.username}
className="search-result-item"
onClick={() => handleUserSelect(user)}
>
{user.name} - {user.username}
</div>
))}
</div>
)}
</div> </div>
); );
}; };

View File

@ -170,16 +170,15 @@
} }
} }
.tag-modal { .available-tags {
.available-tags {
margin-top: 16px; margin-top: 16px;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: visible; overflow: visible;
} }
.tag-list { .tag-list {
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
padding: 8px; padding: 8px;
@ -225,9 +224,9 @@
} }
} }
} }
} }
.tag-item { .tag-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -279,10 +278,10 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
} }
.add-btn, .add-btn,
.remove-btn { .remove-btn {
background: none; background: none;
border: none; border: none;
font-size: 24px; font-size: 24px;
@ -297,9 +296,9 @@
border-radius: 4px; border-radius: 4px;
transition: all 0.2s ease; transition: all 0.2s ease;
flex-shrink: 0; flex-shrink: 0;
} }
.add-btn { .add-btn {
color: var(--default-theme-color, #009933); color: var(--default-theme-color, #009933);
&:hover { &:hover {
@ -313,9 +312,9 @@
background-color: rgba(102, 187, 102, 0.2); background-color: rgba(102, 187, 102, 0.2);
} }
} }
} }
.remove-btn { .remove-btn {
color: #dc3545; color: #dc3545;
&:hover { &:hover {
@ -331,10 +330,10 @@
color: #ff8787; color: #ff8787;
} }
} }
} }
.empty-message, .empty-message,
.loading-message { .loading-message {
padding: 40px 20px; padding: 40px 20px;
text-align: center; text-align: center;
color: #999; color: #999;
@ -344,7 +343,6 @@
.dark_theme & { .dark_theme & {
color: #666; color: #666;
} }
}
} }
.tag-modal-footer { .tag-modal-footer {

View File

@ -7,22 +7,22 @@
.bulk-actions-select { .bulk-actions-select {
width: auto; width: auto;
max-width: 220px; max-width: 220px;
height: 36px; height: 44px;
padding: 0 28px 0 10px; padding: 0 32px 0 10px;
font-size: 13px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
background-color: #f0f0f0; background-color: #f0f0f0;
border: 1px solid #ddd; border: 2px solid #ddd;
border-radius: 4px; border-radius: 6px;
cursor: pointer; cursor: pointer;
appearance: none; appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 8px center; background-position: right 8px center;
background-size: 14px; background-size: 16px;
transition: all 0.2s ease; transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:hover { &:hover {
background-color: #e8e8e8; background-color: #e8e8e8;

View File

@ -11,8 +11,17 @@
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 20px; gap: 0;
padding: 16px 16px 0;
margin-bottom: 16px; margin-bottom: 16px;
@media (min-width: 710px) {
padding: 20px 24px 0;
}
@media (min-width: 476px) {
padding: 16px 0 0;
}
} }
} }

View File

@ -34,7 +34,6 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
onDeselectAll = () => {}, onDeselectAll = () => {},
}) => ( }) => (
<div className={(className ? className + ' ' : '') + 'media-list-wrapper'} style={style}> <div className={(className ? className + ' ' : '') + 'media-list-wrapper'} style={style}>
<MediaListRow title={title} viewAllLink={viewAllLink} viewAllText={viewAllText}>
{showBulkActions && ( {showBulkActions && (
<div className="bulk-actions-container"> <div className="bulk-actions-container">
<BulkActionsDropdown selectedCount={selectedCount} onActionSelect={onBulkAction} /> <BulkActionsDropdown selectedCount={selectedCount} onActionSelect={onBulkAction} />
@ -46,6 +45,7 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
/> />
</div> </div>
)} )}
<MediaListRow title={title} viewAllLink={viewAllLink} viewAllText={viewAllText}>
{children || null} {children || null}
</MediaListRow> </MediaListRow>
</div> </div>

View File

@ -43,7 +43,7 @@ export const SelectAllCheckbox: React.FC<SelectAllCheckboxProps> = ({
disabled={isDisabled} disabled={isDisabled}
aria-label={translateString('Select all media')} aria-label={translateString('Select all media')}
/> />
<span className="checkbox-label-text">{translateString('All')}</span> <span className="checkbox-label-text">{translateString('Select all')}</span>
</label> </label>
</div> </div>
); );

View File

@ -78,7 +78,6 @@ export function listItemProps(props, item, index) {
const url = { const url = {
view: itemPageLink(props, item), view: itemPageLink(props, item),
edit: props.canEdit ? item.url.replace('view?m=', 'edit?m=') : null, edit: props.canEdit ? item.url.replace('view?m=', 'edit?m=') : null,
publish: props.canEdit ? item.url.replace('view?m=', 'publish?m=') : null,
}; };
if (window.MediaCMS.site.devEnv && -1 < url.view.indexOf('view?')) { if (window.MediaCMS.site.devEnv && -1 < url.view.indexOf('view?')) {
@ -322,7 +321,6 @@ export function ListItem(props) {
if (props.canEdit) { if (props.canEdit) {
args.editLink = props.url.edit; args.editLink = props.url.edit;
args.publishLink = props.url.publish;
} }
if (props.taxonomyPage.current) { if (props.taxonomyPage.current) {

View File

@ -28,14 +28,14 @@ export function MediaItem(props) {
(props.hasAnySelection ? ' has-any-selection' : ''); (props.hasAnySelection ? ' has-any-selection' : '');
const handleItemClick = (e) => { const handleItemClick = (e) => {
// Only handle clicks when selection mode is active AND at least one item is selected // Only handle clicks when selection mode is active
if (props.showSelection && props.hasAnySelection) { if (props.showSelection) {
// Check if click was on the checkbox (already handled) // Check if click was on the checkbox (already handled)
if (e.target.type === 'checkbox' || e.target.closest('.item-selection-checkbox')) { if (e.target.type === 'checkbox' || e.target.closest('.item-selection-checkbox')) {
return; return;
} }
// Check if click was on the edit icon or publish icon // Check if click was on the edit icon or view icon
if (e.target.closest('.item-edit-icon') || e.target.closest('.item-view-icon')) { if (e.target.closest('.item-edit-icon') || e.target.closest('.item-view-icon')) {
return; return;
} }
@ -55,11 +55,12 @@ export function MediaItem(props) {
<div className={finalClassname} onClick={handleItemClick}> <div className={finalClassname} onClick={handleItemClick}>
<div className="item-content"> <div className="item-content">
{props.showSelection && ( {props.showSelection && (
<div className="item-selection-checkbox"> <div className="item-selection-checkbox" onClick={(e) => e.stopPropagation()}>
<input <input
type="checkbox" type="checkbox"
checked={props.isSelected || false} checked={props.isSelected || false}
onChange={(e) => { props.onCheckboxChange && props.onCheckboxChange(e); }} onChange={(e) => { props.onCheckboxChange && props.onCheckboxChange(e); }}
onClick={(e) => e.stopPropagation()}
aria-label="Select media" aria-label="Select media"
/> />
</div> </div>

View File

@ -82,8 +82,8 @@ export function MediaItemEditLink(props) {
export function MediaItemViewLink(props) { export function MediaItemViewLink(props) {
return !props.link ? null : ( return !props.link ? null : (
<a href={props.link} title={translateString("Publish media")} className="item-view-icon"> <a href={props.link} title={translateString("View media")} className="item-view-icon">
<i className="material-icons">publish</i> <i className="material-icons">visibility</i>
</a> </a>
); );
} }

View File

@ -66,7 +66,7 @@
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
width: 20%; width: 10%;
&:nth-child(3n + 1), &:nth-child(3n + 1),
&:nth-child(3n + 2), &:nth-child(3n + 2),
@ -98,12 +98,6 @@
padding-right: 0; padding-right: 0;
} }
} }
&.mi-filter-full-width {
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
} }
.mi-filter-title { .mi-filter-title {
@ -156,48 +150,9 @@
&.active button, &.active button,
button:hover { button:hover {
color: var(--default-theme-color); color: inherit;
opacity: 1; opacity: 1;
} }
&.active button {
font-weight: 600;
}
}
&.mi-filter-options-horizontal {
display: flex;
flex-wrap: wrap;
gap: 8px;
> * {
display: inline-block;
margin-top: 0;
margin-right: 8px;
&:last-child {
margin-right: 0;
}
button {
padding: 6px 12px;
border-radius: 4px;
background-color: var(--sidebar-nav-border-color);
opacity: 1;
&:hover {
background-color: var(--default-theme-color);
color: white;
opacity: 0.9;
}
}
&.active button {
background-color: var(--default-theme-color);
color: white;
font-weight: 600;
}
}
} }
} }
} }

View File

@ -35,7 +35,6 @@
li { li {
a { a {
color: var(--profile-page-nav-link-text-color); color: var(--profile-page-nav-link-text-color);
text-transform: none;
&:hover { &:hover {
color: var(--profile-page-nav-link-hover-text-color); color: var(--profile-page-nav-link-hover-text-color);
@ -197,14 +196,14 @@
top: 16px; top: 16px;
right: 16px; right: 16px;
text-decoration: none; text-decoration: none;
color: #fff; color: #666;
border: 0; border: 0;
line-height: 1; line-height: 1;
padding: 0; padding: 0;
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 50%; border-radius: 50%;
background-color: rgba(40, 167, 69, 0.9); background-color: rgba(0, 0, 0, 0.05);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -222,8 +221,8 @@
} }
&:hover { &:hover {
background-color: rgba(40, 167, 69, 1); background-color: rgba(0, 0, 0, 0.1);
color: #fff; color: #333;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: scale(1.05); transform: scale(1.05);
} }
@ -233,11 +232,11 @@
} }
.dark_theme & { .dark_theme & {
background-color: rgba(40, 167, 69, 0.9); background-color: rgba(255, 255, 255, 0.1);
color: #fff; color: #aaa;
&:hover { &:hover {
background-color: rgba(40, 167, 69, 1); background-color: rgba(255, 255, 255, 0.15);
color: #fff; color: #fff;
} }
} }
@ -530,7 +529,7 @@
width: auto; width: auto;
padding: 0 16px; padding: 0 16px;
text-decoration: none; text-decoration: none;
text-transform: none !important; text-transform: uppercase;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;

View File

@ -372,43 +372,19 @@ class NavMenuInlineTabs extends React.PureComponent {
</li> </li>
{this.props.onToggleFiltersClick && ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? ( {this.props.onToggleFiltersClick && ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<li className="media-filters-toggle"> <li className="media-filters-toggle">
<span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', position: 'relative' }} onClick={this.props.onToggleFiltersClick} title={translateString('Filters')}> <span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={this.props.onToggleFiltersClick} title={translateString('Filters')}>
<CircleIconButton buttonShadow={false}> <CircleIconButton buttonShadow={false}>
<i className="material-icons">filter_list</i> <i className="material-icons">filter_list</i>
</CircleIconButton> </CircleIconButton>
{this.props.hasActiveFilters ? (
<span style={{
position: 'absolute',
top: '8px',
right: '8px',
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: 'var(--default-theme-color)',
border: '2px solid white',
}}></span>
) : null}
</span> </span>
</li> </li>
) : null} ) : null}
{this.props.onToggleTagsClick && ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? ( {this.props.onToggleTagsClick && ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<li className="media-tags-toggle"> <li className="media-tags-toggle">
<span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', position: 'relative' }} onClick={this.props.onToggleTagsClick} title={translateString('Tags')}> <span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={this.props.onToggleTagsClick} title={translateString('Tags')}>
<CircleIconButton buttonShadow={false}> <CircleIconButton buttonShadow={false}>
<i className="material-icons">local_offer</i> <i className="material-icons">local_offer</i>
</CircleIconButton> </CircleIconButton>
{this.props.hasActiveTags ? (
<span style={{
position: 'absolute',
top: '8px',
right: '8px',
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: 'var(--default-theme-color)',
border: '2px solid white',
}}></span>
) : null}
</span> </span>
</li> </li>
) : null} ) : null}
@ -436,8 +412,6 @@ NavMenuInlineTabs.propTypes = {
onToggleFiltersClick: PropTypes.func, onToggleFiltersClick: PropTypes.func,
onToggleTagsClick: PropTypes.func, onToggleTagsClick: PropTypes.func,
onToggleSortingClick: PropTypes.func, onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool,
hasActiveTags: PropTypes.bool,
}; };
function AddBannerButton(props) { function AddBannerButton(props) {
@ -600,7 +574,7 @@ export default function ProfilePagesHeader(props) {
></span> ></span>
) : null} ) : null}
{userCanDeleteProfile && !userIsAuthor ? ( {userCanDeleteProfile ? (
<span className="delete-profile-wrap"> <span className="delete-profile-wrap">
<PopupTrigger contentRef={popupContentRef}> <PopupTrigger contentRef={popupContentRef}>
<button className="delete-profile" title="Remove profile"> <button className="delete-profile" title="Remove profile">
@ -628,7 +602,7 @@ export default function ProfilePagesHeader(props) {
</span> </span>
) : null} ) : null}
{userCanEditProfile && userIsAuthor ? ( {userCanEditProfile ? (
props.author.banner_thumbnail_url ? ( props.author.banner_thumbnail_url ? (
<EditBannerButton link={ProfilePageStore.get('author-data').default_channel_edit_url} /> <EditBannerButton link={ProfilePageStore.get('author-data').default_channel_edit_url} />
) : ( ) : (
@ -646,7 +620,7 @@ export default function ProfilePagesHeader(props) {
{props.author.name ? ( {props.author.name ? (
<div className="profile-name-edit-wrapper"> <div className="profile-name-edit-wrapper">
<h1>{props.author.name}</h1> <h1>{props.author.name}</h1>
{userCanEditProfile && !userIsAuthor ? <EditProfileButton link={ProfilePageStore.get('author-data').edit_url} /> : null} {userCanEditProfile ? <EditProfileButton link={ProfilePageStore.get('author-data').edit_url} /> : null}
</div> </div>
) : null} ) : null}
</div> </div>
@ -661,8 +635,6 @@ export default function ProfilePagesHeader(props) {
onToggleFiltersClick={props.onToggleFiltersClick} onToggleFiltersClick={props.onToggleFiltersClick}
onToggleTagsClick={props.onToggleTagsClick} onToggleTagsClick={props.onToggleTagsClick}
onToggleSortingClick={props.onToggleSortingClick} onToggleSortingClick={props.onToggleSortingClick}
hasActiveFilters={props.hasActiveFilters}
hasActiveTags={props.hasActiveTags}
/> />
</div> </div>
</div> </div>
@ -676,8 +648,6 @@ ProfilePagesHeader.propTypes = {
onToggleFiltersClick: PropTypes.func, onToggleFiltersClick: PropTypes.func,
onToggleTagsClick: PropTypes.func, onToggleTagsClick: PropTypes.func,
onToggleSortingClick: PropTypes.func, onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool,
hasActiveTags: PropTypes.bool,
}; };
ProfilePagesHeader.defaultProps = { ProfilePagesHeader.defaultProps = {

View File

@ -72,8 +72,8 @@ export function ProfileMediaFilters(props) {
upload_date: uploadDateFilter, upload_date: uploadDateFilter,
duration: durationFilter, duration: durationFilter,
publish_state: publishStateFilter, publish_state: publishStateFilter,
sort_by: props.selectedSort || sortByFilter, sort_by: sortByFilter,
tag: props.selectedTag || tagFilter, tag: tagFilter,
}; };
switch (ev.currentTarget.getAttribute('filter')) { switch (ev.currentTarget.getAttribute('filter')) {
@ -180,8 +180,6 @@ ProfileMediaFilters.propTypes = {
hidden: PropTypes.bool, hidden: PropTypes.bool,
tags: PropTypes.array, tags: PropTypes.array,
onFiltersUpdate: PropTypes.func.isRequired, onFiltersUpdate: PropTypes.func.isRequired,
selectedTag: PropTypes.string,
selectedSort: PropTypes.string,
}; };
ProfileMediaFilters.defaultProps = { ProfileMediaFilters.defaultProps = {

View File

@ -43,9 +43,9 @@ export function ProfileMediaTags(props) {
return ( return (
<div ref={containerRef} className={'mi-filters-row' + (isHidden ? ' hidden' : '')}> <div ref={containerRef} className={'mi-filters-row' + (isHidden ? ' hidden' : '')}>
<div ref={innerContainerRef} className="mi-filters-row-inner"> <div ref={innerContainerRef} className="mi-filters-row-inner">
<div className="mi-filter mi-filter-full-width"> <div className="mi-filter">
<div className="mi-filter-title">{translateString('TAGS')}</div> <div className="mi-filter-title">{translateString('TAGS')}</div>
<div className="mi-filter-options mi-filter-options-horizontal"> <div className="mi-filter-options" style={tagsOptions.length >= 10 ? { maxHeight: '300px', overflowY: 'auto' } : {}}>
<FilterOptions id={'tag'} options={tagsOptions} selected={tagFilter} onSelect={onFilterSelect} /> <FilterOptions id={'tag'} options={tagsOptions} selected={tagFilter} onSelect={onFilterSelect} />
</div> </div>
</div> </div>

View File

@ -583,23 +583,17 @@ export class ProfileMediaPage extends Page {
onToggleFiltersClick() { onToggleFiltersClick() {
this.setState({ this.setState({
hiddenFilters: !this.state.hiddenFilters, hiddenFilters: !this.state.hiddenFilters,
hiddenTags: true,
hiddenSorting: true,
}); });
} }
onToggleTagsClick() { onToggleTagsClick() {
this.setState({ this.setState({
hiddenFilters: true,
hiddenTags: !this.state.hiddenTags, hiddenTags: !this.state.hiddenTags,
hiddenSorting: true,
}); });
} }
onToggleSortingClick() { onToggleSortingClick() {
this.setState({ this.setState({
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: !this.state.hiddenSorting, hiddenSorting: !this.state.hiddenSorting,
}); });
} }
@ -875,23 +869,6 @@ export class ProfileMediaPage extends Page {
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username; const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
// Check if any filters are active (excluding default sort and tags)
const hasActiveFilters = this.state.filterArgs && (
this.state.filterArgs.includes('media_type=') ||
this.state.filterArgs.includes('upload_date=') ||
this.state.filterArgs.includes('duration=') ||
this.state.filterArgs.includes('publish_state=')
);
const hasActiveTags = this.state.selectedTag && this.state.selectedTag !== 'all';
console.log('Filter Debug:', {
filterArgs: this.state.filterArgs,
selectedTag: this.state.selectedTag,
hasActiveFilters,
hasActiveTags
});
return [ return [
this.state.author ? ( this.state.author ? (
<ProfilePagesHeader <ProfilePagesHeader
@ -902,14 +879,20 @@ export class ProfileMediaPage extends Page {
onToggleFiltersClick={this.onToggleFiltersClick} onToggleFiltersClick={this.onToggleFiltersClick}
onToggleTagsClick={this.onToggleTagsClick} onToggleTagsClick={this.onToggleTagsClick}
onToggleSortingClick={this.onToggleSortingClick} onToggleSortingClick={this.onToggleSortingClick}
hasActiveFilters={hasActiveFilters}
hasActiveTags={hasActiveTags}
/> />
) : null, ) : null,
this.state.author ? ( this.state.author ? (
<ProfilePagesContent key="ProfilePagesContent"> <ProfilePagesContent key="ProfilePagesContent">
<MediaListWrapper <MediaListWrapper
title={this.state.title} title={
!isMediaAuthor
? this.state.title
: 0 < this.state.channelMediaCount
? this.state.title === 'Uploads'
? null
: this.state.title
: null
}
className="items-list-ver" className="items-list-ver"
showBulkActions={isMediaAuthor} showBulkActions={isMediaAuthor}
selectedCount={this.state.selectedMedia.size} selectedCount={this.state.selectedMedia.size}
@ -918,7 +901,7 @@ export class ProfileMediaPage extends Page {
onSelectAll={this.handleSelectAll} onSelectAll={this.handleSelectAll}
onDeselectAll={this.handleDeselectAll} onDeselectAll={this.handleDeselectAll}
> >
<ProfileMediaFilters hidden={this.state.hiddenFilters} tags={this.state.availableTags} onFiltersUpdate={this.onFiltersUpdate} selectedTag={this.state.selectedTag} selectedSort={this.state.selectedSort} /> <ProfileMediaFilters hidden={this.state.hiddenFilters} tags={this.state.availableTags} onFiltersUpdate={this.onFiltersUpdate} />
<ProfileMediaTags hidden={this.state.hiddenTags} tags={this.state.availableTags} onTagSelect={this.onTagSelect} /> <ProfileMediaTags hidden={this.state.hiddenTags} tags={this.state.availableTags} onTagSelect={this.onTagSelect} />
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} /> <ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
<LazyLoadItemListAsync <LazyLoadItemListAsync

View File

@ -35,7 +35,7 @@ export function useMediaItem(props) {
} }
function viewMediaComponent() { function viewMediaComponent() {
return props.showSelection ? <MediaItemViewLink link={props.publishLink || props.link} /> : null; return props.showSelection ? <MediaItemViewLink link={props.link} /> : null;
} }
function authorComponent() { function authorComponent() {

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

@ -12,146 +12,6 @@
{{ form.as_p }} {{ form.as_p }}
<button class="primaryAction" type="submit">Update Profile</button> <button class="primaryAction" type="submit">Update Profile</button>
</form> </form>
{% if is_author %}
<div class="danger-zone">
<h2>Danger Zone</h2>
<div class="danger-zone-content">
<div class="danger-zone-info">
<h3>Delete Account</h3>
<p>This will permanently remove all data, including media and playlists.</p>
</div>
<button class="btn-danger" id="delete-account-btn" type="button">Delete Account</button>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
<style>
.danger-zone {
margin-top: 60px;
padding-top: 40px;
border-top: 2px solid #e0e0e0;
}
.danger-zone h2 {
color: #d32f2f;
font-size: 20px;
margin-bottom: 20px;
font-weight: 600;
}
.danger-zone-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border: 2px solid #ffcdd2;
border-radius: 8px;
background-color: #ffebee;
gap: 20px;
}
.dark_theme .danger-zone-content {
background-color: #3a1f1f;
border-color: #5a2d2d;
}
.danger-zone-info h3 {
font-size: 16px;
font-weight: 600;
margin: 0 0 8px 0;
color: #d32f2f;
}
.danger-zone-info p {
font-size: 14px;
margin: 0;
color: #666;
line-height: 1.5;
}
.dark_theme .danger-zone-info p {
color: #ccc;
}
.btn-danger {
padding: 10px 24px;
background-color: #d32f2f;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-danger:hover {
background-color: #b71c1c;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: translateY(-1px);
}
.btn-danger:active {
transform: translateY(0);
}
@media (max-width: 768px) {
.danger-zone-content {
flex-direction: column;
align-items: flex-start;
}
.btn-danger {
width: 100%;
}
}
</style>
<script>
document.getElementById('delete-account-btn').addEventListener('click', function() {
const username = '{{ user.username }}';
const confirmText = 'Are you sure you want to delete your account?\n\n' +
'This action CANNOT be undone. This will permanently delete:\n' +
'• Your profile and all personal information\n' +
'• All your uploaded media\n' +
'• All your playlists\n' +
'• All your comments and interactions\n\n' +
'Type "DELETE" to confirm:';
const userInput = prompt(confirmText);
if (userInput === 'DELETE') {
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
fetch('/api/v1/users/' + username, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
})
.then(function(response) {
if (response.ok) {
alert('Your account has been deleted successfully. You will be redirected to the home page.');
window.location.href = '/';
} else {
return response.json().then(function(data) {
throw new Error(data.detail || 'Failed to delete account.');
});
}
})
.catch(function(error) {
alert('Error deleting account: ' + error.message);
console.error('Error deleting account:', error);
});
} else if (userInput !== null) {
alert('Account deletion cancelled. You must type "DELETE" exactly to confirm.');
}
});
</script>
{% endblock innercontent %} {% endblock innercontent %}

View File

@ -102,7 +102,6 @@ def view_user_about(request, username):
@login_required @login_required
def edit_user(request, username): def edit_user(request, username):
context = {}
user = get_user(username=username) user = get_user(username=username)
if not user or (user != request.user and not is_mediacms_manager(request.user)): if not user or (user != request.user and not is_mediacms_manager(request.user)):
return HttpResponseRedirect("/") return HttpResponseRedirect("/")
@ -115,13 +114,7 @@ def edit_user(request, username):
return HttpResponseRedirect(user.get_absolute_url()) return HttpResponseRedirect(user.get_absolute_url())
else: else:
form = UserForm(request.user, instance=user) form = UserForm(request.user, instance=user)
context["form"] = form return render(request, "cms/user_edit.html", {"form": form})
context["user"] = user
if user == request.user:
context["is_author"] = True
else:
context["is_author"] = False
return render(request, "cms/user_edit.html", context)
def view_channel(request, friendly_token): def view_channel(request, friendly_token):