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

View File

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

View File

@ -193,11 +193,8 @@ class MediaList(APIView):
already_sorted = True
else:
if is_mediacms_editor(self.request.user):
media = Media.objects.prefetch_related("user", "tags")
else:
media = self._get_media_queryset(request)
already_sorted = True
media = self._get_media_queryset(request)
already_sorted = True
if tag:
media = media.filter(tags__title=tag)
@ -1013,15 +1010,11 @@ class MediaSearch(APIView):
return Response(ret, status=status.HTTP_200_OK)
if request.user.is_authenticated:
if is_mediacms_editor(self.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)
basic_query = Q(listable=True) | Q(permissions__user=request.user)
if getattr(settings, 'USE_RBAC', False):
rbac_categories = request.user.get_rbac_categories_as_member()
basic_query |= Q(category__in=rbac_categories)
if getattr(settings, 'USE_RBAC', False):
rbac_categories = request.user.get_rbac_categories_as_member()
basic_query |= Q(category__in=rbac_categories)
else:
basic_query = Q(listable=True)

View File

@ -170,180 +170,178 @@
}
}
.category-modal {
.available-categories {
margin-top: 16px;
flex: 1;
display: flex;
flex-direction: column;
overflow: visible;
.available-categories {
margin-top: 16px;
flex: 1;
display: flex;
flex-direction: column;
overflow: visible;
}
.category-list {
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
background-color: #f9f9f9;
min-height: 100px;
.dark_theme & {
background-color: #333;
border-color: #555;
}
.category-list {
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
background-color: #f9f9f9;
min-height: 100px;
&.scrollable {
max-height: 300px;
overflow-y: auto;
.dark_theme & {
background-color: #333;
border-color: #555;
&::-webkit-scrollbar {
width: 8px;
}
&.scrollable {
max-height: 300px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
.dark_theme & {
background: #2a2a2a;
}
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
&:hover {
background: #aaa;
}
.dark_theme & {
background: #555;
&:hover {
background: #666;
}
}
}
}
}
.category-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
margin-bottom: 6px;
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s ease;
.dark_theme & {
background-color: #2a2a2a;
border-color: #444;
color: #fff;
}
&.clickable {
cursor: pointer;
&:hover {
background-color: #f0f7ff;
border-color: var(--default-theme-color, #009933);
.dark_theme & {
background-color: #3a3a3a;
}
}
}
&.marked-for-removal {
background-color: #ffe0e0;
border-color: #ffaaaa;
opacity: 0.7;
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
.dark_theme & {
background-color: #4a2a2a;
border-color: #aa5555;
background: #2a2a2a;
}
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
&:hover {
background: #aaa;
}
span {
text-decoration: line-through;
.dark_theme & {
background: #555;
&:hover {
background: #666;
}
}
}
}
}
.category-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
margin-bottom: 6px;
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s ease;
.dark_theme & {
background-color: #2a2a2a;
border-color: #444;
color: #fff;
}
&.clickable {
cursor: pointer;
&:hover {
background-color: #f0f7ff;
border-color: var(--default-theme-color, #009933);
.dark_theme & {
background-color: #3a3a3a;
}
}
}
&.marked-for-removal {
background-color: #ffe0e0;
border-color: #ffaaaa;
opacity: 0.7;
.dark_theme & {
background-color: #4a2a2a;
border-color: #aa5555;
}
span {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-decoration: line-through;
}
}
.add-btn,
.remove-btn {
background: none;
border: none;
font-size: 24px;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
flex-shrink: 0;
span {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.add-btn,
.remove-btn {
background: none;
border: none;
font-size: 24px;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
flex-shrink: 0;
}
.add-btn {
color: var(--default-theme-color, #009933);
&:hover {
background-color: rgba(0, 153, 51, 0.1);
}
.add-btn {
color: var(--default-theme-color, #009933);
.dark_theme & {
color: #66bb66;
&:hover {
background-color: rgba(0, 153, 51, 0.1);
}
.dark_theme & {
color: #66bb66;
&:hover {
background-color: rgba(102, 187, 102, 0.2);
}
background-color: rgba(102, 187, 102, 0.2);
}
}
}
.remove-btn {
color: #dc3545;
.remove-btn {
color: #dc3545;
&:hover {
background-color: rgba(220, 53, 69, 0.1);
color: #c82333;
}
.dark_theme & {
color: #ff6b6b;
&:hover {
background-color: rgba(220, 53, 69, 0.1);
color: #c82333;
}
.dark_theme & {
color: #ff6b6b;
&:hover {
background-color: rgba(255, 107, 107, 0.2);
color: #ff8787;
}
background-color: rgba(255, 107, 107, 0.2);
color: #ff8787;
}
}
}
.empty-message,
.loading-message {
padding: 40px 20px;
text-align: center;
color: #999;
font-size: 14px;
font-style: italic;
.empty-message,
.loading-message {
padding: 40px 20px;
text-align: center;
color: #999;
font-size: 14px;
font-style: italic;
.dark_theme & {
color: #666;
}
.dark_theme & {
color: #666;
}
}

View File

@ -144,17 +144,16 @@
}
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
position: fixed;
left: auto;
right: auto;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 4px;
max-height: 250px;
overflow-y: auto;
z-index: 1000;
z-index: 10001;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
.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 { translateString } from '../utils/helpers/';
@ -29,6 +29,8 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
const searchBoxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen) {
@ -39,6 +41,17 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
}
}, [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) => {
if (!name.trim()) {
setSearchResults([]);
@ -53,6 +66,7 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
const data = await response.json();
setSearchResults(data.results || data);
updateDropdownPosition();
} catch (error) {
console.error('Error searching users:', error);
}
@ -130,7 +144,7 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
</div>
<div className="change-owner-modal-content">
<div className="search-box-wrapper">
<div className="search-box-wrapper" ref={searchBoxRef}>
<div className="search-box">
<input
type="text"
@ -139,19 +153,6 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
onChange={(e) => handleSearchChange(e.target.value)}
/>
</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>
{selectedUser && (
@ -174,6 +175,27 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
</button>
</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>
);
};

View File

@ -170,180 +170,178 @@
}
}
.tag-modal {
.available-tags {
margin-top: 16px;
flex: 1;
display: flex;
flex-direction: column;
overflow: visible;
.available-tags {
margin-top: 16px;
flex: 1;
display: flex;
flex-direction: column;
overflow: visible;
}
.tag-list {
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
background-color: #f9f9f9;
min-height: 100px;
.dark_theme & {
background-color: #333;
border-color: #555;
}
.tag-list {
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
background-color: #f9f9f9;
min-height: 100px;
&.scrollable {
max-height: 300px;
overflow-y: auto;
.dark_theme & {
background-color: #333;
border-color: #555;
&::-webkit-scrollbar {
width: 8px;
}
&.scrollable {
max-height: 300px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
.dark_theme & {
background: #2a2a2a;
}
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
&:hover {
background: #aaa;
}
.dark_theme & {
background: #555;
&:hover {
background: #666;
}
}
}
}
}
.tag-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
margin-bottom: 6px;
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s ease;
.dark_theme & {
background-color: #2a2a2a;
border-color: #444;
color: #fff;
}
&.clickable {
cursor: pointer;
&:hover {
background-color: #f0f7ff;
border-color: var(--default-theme-color, #009933);
.dark_theme & {
background-color: #3a3a3a;
}
}
}
&.marked-for-removal {
background-color: #ffe0e0;
border-color: #ffaaaa;
opacity: 0.7;
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
.dark_theme & {
background-color: #4a2a2a;
border-color: #aa5555;
background: #2a2a2a;
}
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
&:hover {
background: #aaa;
}
span {
text-decoration: line-through;
.dark_theme & {
background: #555;
&:hover {
background: #666;
}
}
}
}
}
.tag-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
margin-bottom: 6px;
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s ease;
.dark_theme & {
background-color: #2a2a2a;
border-color: #444;
color: #fff;
}
&.clickable {
cursor: pointer;
&:hover {
background-color: #f0f7ff;
border-color: var(--default-theme-color, #009933);
.dark_theme & {
background-color: #3a3a3a;
}
}
}
&.marked-for-removal {
background-color: #ffe0e0;
border-color: #ffaaaa;
opacity: 0.7;
.dark_theme & {
background-color: #4a2a2a;
border-color: #aa5555;
}
span {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-decoration: line-through;
}
}
.add-btn,
.remove-btn {
background: none;
border: none;
font-size: 24px;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
flex-shrink: 0;
span {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.add-btn,
.remove-btn {
background: none;
border: none;
font-size: 24px;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
flex-shrink: 0;
}
.add-btn {
color: var(--default-theme-color, #009933);
&:hover {
background-color: rgba(0, 153, 51, 0.1);
}
.add-btn {
color: var(--default-theme-color, #009933);
.dark_theme & {
color: #66bb66;
&:hover {
background-color: rgba(0, 153, 51, 0.1);
}
.dark_theme & {
color: #66bb66;
&:hover {
background-color: rgba(102, 187, 102, 0.2);
}
background-color: rgba(102, 187, 102, 0.2);
}
}
}
.remove-btn {
color: #dc3545;
.remove-btn {
color: #dc3545;
&:hover {
background-color: rgba(220, 53, 69, 0.1);
color: #c82333;
}
.dark_theme & {
color: #ff6b6b;
&:hover {
background-color: rgba(220, 53, 69, 0.1);
color: #c82333;
}
.dark_theme & {
color: #ff6b6b;
&:hover {
background-color: rgba(255, 107, 107, 0.2);
color: #ff8787;
}
background-color: rgba(255, 107, 107, 0.2);
color: #ff8787;
}
}
}
.empty-message,
.loading-message {
padding: 40px 20px;
text-align: center;
color: #999;
font-size: 14px;
font-style: italic;
.empty-message,
.loading-message {
padding: 40px 20px;
text-align: center;
color: #999;
font-size: 14px;
font-style: italic;
.dark_theme & {
color: #666;
}
.dark_theme & {
color: #666;
}
}

View File

@ -7,22 +7,22 @@
.bulk-actions-select {
width: auto;
max-width: 220px;
height: 36px;
padding: 0 28px 0 10px;
font-size: 13px;
height: 44px;
padding: 0 32px 0 10px;
font-size: 14px;
font-weight: 600;
color: #333;
background-color: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
border: 2px solid #ddd;
border-radius: 6px;
cursor: pointer;
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-repeat: no-repeat;
background-position: right 8px center;
background-size: 14px;
background-size: 16px;
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 {
background-color: #e8e8e8;

View File

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

View File

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

View File

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

View File

@ -78,7 +78,6 @@ export function listItemProps(props, item, index) {
const url = {
view: itemPageLink(props, item),
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?')) {
@ -322,7 +321,6 @@ export function ListItem(props) {
if (props.canEdit) {
args.editLink = props.url.edit;
args.publishLink = props.url.publish;
}
if (props.taxonomyPage.current) {

View File

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

View File

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

View File

@ -66,7 +66,7 @@
}
@media (min-width: 1024px) {
width: 20%;
width: 10%;
&:nth-child(3n + 1),
&:nth-child(3n + 2),
@ -98,12 +98,6 @@
padding-right: 0;
}
}
&.mi-filter-full-width {
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
}
.mi-filter-title {
@ -156,48 +150,9 @@
&.active button,
button:hover {
color: var(--default-theme-color);
color: inherit;
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 {
a {
color: var(--profile-page-nav-link-text-color);
text-transform: none;
&:hover {
color: var(--profile-page-nav-link-hover-text-color);
@ -197,14 +196,14 @@
top: 16px;
right: 16px;
text-decoration: none;
color: #fff;
color: #666;
border: 0;
line-height: 1;
padding: 0;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: rgba(40, 167, 69, 0.9);
background-color: rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
justify-content: center;
@ -222,8 +221,8 @@
}
&:hover {
background-color: rgba(40, 167, 69, 1);
color: #fff;
background-color: rgba(0, 0, 0, 0.1);
color: #333;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: scale(1.05);
}
@ -233,11 +232,11 @@
}
.dark_theme & {
background-color: rgba(40, 167, 69, 0.9);
color: #fff;
background-color: rgba(255, 255, 255, 0.1);
color: #aaa;
&:hover {
background-color: rgba(40, 167, 69, 1);
background-color: rgba(255, 255, 255, 0.15);
color: #fff;
}
}
@ -530,7 +529,7 @@
width: auto;
padding: 0 16px;
text-decoration: none;
text-transform: none !important;
text-transform: uppercase;
font-size: 14px;
font-weight: 500;

View File

@ -372,43 +372,19 @@ class NavMenuInlineTabs extends React.PureComponent {
</li>
{this.props.onToggleFiltersClick && ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<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}>
<i className="material-icons">filter_list</i>
</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>
</li>
) : null}
{this.props.onToggleTagsClick && ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<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}>
<i className="material-icons">local_offer</i>
</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>
</li>
) : null}
@ -436,8 +412,6 @@ NavMenuInlineTabs.propTypes = {
onToggleFiltersClick: PropTypes.func,
onToggleTagsClick: PropTypes.func,
onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool,
hasActiveTags: PropTypes.bool,
};
function AddBannerButton(props) {
@ -600,7 +574,7 @@ export default function ProfilePagesHeader(props) {
></span>
) : null}
{userCanDeleteProfile && !userIsAuthor ? (
{userCanDeleteProfile ? (
<span className="delete-profile-wrap">
<PopupTrigger contentRef={popupContentRef}>
<button className="delete-profile" title="Remove profile">
@ -628,7 +602,7 @@ export default function ProfilePagesHeader(props) {
</span>
) : null}
{userCanEditProfile && userIsAuthor ? (
{userCanEditProfile ? (
props.author.banner_thumbnail_url ? (
<EditBannerButton link={ProfilePageStore.get('author-data').default_channel_edit_url} />
) : (
@ -646,7 +620,7 @@ export default function ProfilePagesHeader(props) {
{props.author.name ? (
<div className="profile-name-edit-wrapper">
<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>
) : null}
</div>
@ -661,8 +635,6 @@ export default function ProfilePagesHeader(props) {
onToggleFiltersClick={props.onToggleFiltersClick}
onToggleTagsClick={props.onToggleTagsClick}
onToggleSortingClick={props.onToggleSortingClick}
hasActiveFilters={props.hasActiveFilters}
hasActiveTags={props.hasActiveTags}
/>
</div>
</div>
@ -676,8 +648,6 @@ ProfilePagesHeader.propTypes = {
onToggleFiltersClick: PropTypes.func,
onToggleTagsClick: PropTypes.func,
onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool,
hasActiveTags: PropTypes.bool,
};
ProfilePagesHeader.defaultProps = {

View File

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

View File

@ -43,9 +43,9 @@ export function ProfileMediaTags(props) {
return (
<div ref={containerRef} className={'mi-filters-row' + (isHidden ? ' hidden' : '')}>
<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-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} />
</div>
</div>

View File

@ -583,23 +583,17 @@ export class ProfileMediaPage extends Page {
onToggleFiltersClick() {
this.setState({
hiddenFilters: !this.state.hiddenFilters,
hiddenTags: true,
hiddenSorting: true,
});
}
onToggleTagsClick() {
this.setState({
hiddenFilters: true,
hiddenTags: !this.state.hiddenTags,
hiddenSorting: true,
});
}
onToggleSortingClick() {
this.setState({
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: !this.state.hiddenSorting,
});
}
@ -875,23 +869,6 @@ export class ProfileMediaPage extends Page {
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 [
this.state.author ? (
<ProfilePagesHeader
@ -902,14 +879,20 @@ export class ProfileMediaPage extends Page {
onToggleFiltersClick={this.onToggleFiltersClick}
onToggleTagsClick={this.onToggleTagsClick}
onToggleSortingClick={this.onToggleSortingClick}
hasActiveFilters={hasActiveFilters}
hasActiveTags={hasActiveTags}
/>
) : null,
this.state.author ? (
<ProfilePagesContent key="ProfilePagesContent">
<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"
showBulkActions={isMediaAuthor}
selectedCount={this.state.selectedMedia.size}
@ -918,7 +901,7 @@ export class ProfileMediaPage extends Page {
onSelectAll={this.handleSelectAll}
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} />
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
<LazyLoadItemListAsync

View File

@ -35,7 +35,7 @@ export function useMediaItem(props) {
}
function viewMediaComponent() {
return props.showSelection ? <MediaItemViewLink link={props.publishLink || props.link} /> : null;
return props.showSelection ? <MediaItemViewLink link={props.link} /> : null;
}
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 }}
<button class="primaryAction" type="submit">Update Profile</button>
</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>
<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 %}

View File

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