Compare commits

...

16 Commits

Author SHA1 Message Date
Markos Gogoulos
930b80079e fix 2025-10-18 19:15:59 +03:00
Markos Gogoulos
48fe482897 fix 2025-10-18 19:15:09 +03:00
Markos Gogoulos
28c1d4ee44 fix 2025-10-18 19:00:51 +03:00
Markos Gogoulos
61925bbd6e fix 2025-10-18 18:43:20 +03:00
Markos Gogoulos
2ea45bfd78 fix 2025-10-18 18:37:04 +03:00
Markos Gogoulos
004584de03 fix 2025-10-18 18:29:10 +03:00
Markos Gogoulos
9372398ab5 fix 2025-10-18 18:19:52 +03:00
Markos Gogoulos
b3d9776985 fix 2025-10-18 17:31:39 +03:00
Markos Gogoulos
0f6d965f54 fix 2025-10-18 17:02:46 +03:00
Markos Gogoulos
17b8c60450 fix 2025-10-18 16:53:09 +03:00
Markos Gogoulos
cd173fc38e fix 2025-10-18 16:50:55 +03:00
Markos Gogoulos
c39e8e26dd fix 2025-10-18 16:36:18 +03:00
Markos Gogoulos
a40232e43b amdin 2025-10-18 16:36:17 +03:00
Markos Gogoulos
870274e676 fix 2025-10-18 16:29:42 +03:00
Markos Gogoulos
d876084e5c fixes 2025-10-18 15:41:18 +03:00
Markos Gogoulos
f48166b427 fixes 2025-10-18 15:07:41 +03:00
34 changed files with 652 additions and 412 deletions

View File

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

View File

@ -205,7 +205,11 @@ class SubtitleAdmin(admin.ModelAdmin):
class VideoTrimRequestAdmin(admin.ModelAdmin):
pass
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",)
class EncodingAdmin(admin.ModelAdmin):
@ -224,7 +228,11 @@ class EncodingAdmin(admin.ModelAdmin):
class TranscriptionRequestAdmin(admin.ModelAdmin):
pass
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",)
class PageAdminForm(forms.ModelForm):

View File

@ -80,5 +80,9 @@ 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,6 +77,10 @@ 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

@ -192,6 +192,9 @@ class MediaList(APIView):
media = self._get_media_queryset(request, user)
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
@ -1010,7 +1013,11 @@ class MediaSearch(APIView):
return Response(ret, status=status.HTTP_200_OK)
if request.user.is_authenticated:
basic_query = Q(listable=True) | Q(permissions__user=request.user)
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)
if getattr(settings, 'USE_RBAC', False):
rbac_categories = request.user.get_rbac_categories_as_member()

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect } from 'react';
import './BulkActionChangeOwnerModal.scss';
import { translateString } from '../utils/helpers/';
@ -29,8 +29,6 @@ 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) {
@ -41,17 +39,6 @@ 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([]);
@ -66,7 +53,6 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
const data = await response.json();
setSearchResults(data.results || data);
updateDropdownPosition();
} catch (error) {
console.error('Error searching users:', error);
}
@ -144,7 +130,7 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
</div>
<div className="change-owner-modal-content">
<div className="search-box-wrapper" ref={searchBoxRef}>
<div className="search-box-wrapper">
<div className="search-box">
<input
type="text"
@ -153,6 +139,19 @@ 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 && (
@ -175,27 +174,6 @@ 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,15 +170,16 @@
}
}
.available-tags {
.tag-modal {
.available-tags {
margin-top: 16px;
flex: 1;
display: flex;
flex-direction: column;
overflow: visible;
}
}
.tag-list {
.tag-list {
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
@ -224,9 +225,9 @@
}
}
}
}
}
.tag-item {
.tag-item {
display: flex;
justify-content: space-between;
align-items: center;
@ -278,10 +279,10 @@
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.add-btn,
.remove-btn {
.add-btn,
.remove-btn {
background: none;
border: none;
font-size: 24px;
@ -296,9 +297,9 @@
border-radius: 4px;
transition: all 0.2s ease;
flex-shrink: 0;
}
}
.add-btn {
.add-btn {
color: var(--default-theme-color, #009933);
&:hover {
@ -312,9 +313,9 @@
background-color: rgba(102, 187, 102, 0.2);
}
}
}
}
.remove-btn {
.remove-btn {
color: #dc3545;
&:hover {
@ -330,10 +331,10 @@
color: #ff8787;
}
}
}
}
.empty-message,
.loading-message {
.empty-message,
.loading-message {
padding: 40px 20px;
text-align: center;
color: #999;
@ -343,6 +344,7 @@
.dark_theme & {
color: #666;
}
}
}
.tag-modal-footer {

View File

@ -7,22 +7,22 @@
.bulk-actions-select {
width: auto;
max-width: 220px;
height: 44px;
padding: 0 32px 0 10px;
font-size: 14px;
height: 36px;
padding: 0 28px 0 10px;
font-size: 13px;
font-weight: 600;
color: #333;
background-color: #f0f0f0;
border: 2px solid #ddd;
border-radius: 6px;
border: 1px solid #ddd;
border-radius: 4px;
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: 16px;
background-size: 14px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
&:hover {
background-color: #e8e8e8;

View File

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

View File

@ -34,6 +34,7 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
onDeselectAll = () => {},
}) => (
<div className={(className ? className + ' ' : '') + 'media-list-wrapper'} style={style}>
<MediaListRow title={title} viewAllLink={viewAllLink} viewAllText={viewAllText}>
{showBulkActions && (
<div className="bulk-actions-container">
<BulkActionsDropdown selectedCount={selectedCount} onActionSelect={onBulkAction} />
@ -45,7 +46,6 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
/>
</div>
)}
<MediaListRow title={title} viewAllLink={viewAllLink} viewAllText={viewAllText}>
{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('Select all')}</span>
<span className="checkbox-label-text">{translateString('All')}</span>
</label>
</div>
);

View File

@ -78,6 +78,7 @@ 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?')) {
@ -321,6 +322,7 @@ 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
if (props.showSelection) {
// Only handle clicks when selection mode is active AND at least one item is selected
if (props.showSelection && props.hasAnySelection) {
// 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 view icon
// Check if click was on the edit icon or publish icon
if (e.target.closest('.item-edit-icon') || e.target.closest('.item-view-icon')) {
return;
}
@ -55,12 +55,11 @@ export function MediaItem(props) {
<div className={finalClassname} onClick={handleItemClick}>
<div className="item-content">
{props.showSelection && (
<div className="item-selection-checkbox" onClick={(e) => e.stopPropagation()}>
<div className="item-selection-checkbox">
<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("View media")} className="item-view-icon">
<i className="material-icons">visibility</i>
<a href={props.link} title={translateString("Publish media")} className="item-view-icon">
<i className="material-icons">publish</i>
</a>
);
}

View File

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

View File

@ -372,19 +372,43 @@ 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' }} onClick={this.props.onToggleFiltersClick} title={translateString('Filters')}>
<span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', position: 'relative' }} 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' }} onClick={this.props.onToggleTagsClick} title={translateString('Tags')}>
<span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', position: 'relative' }} 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}
@ -412,6 +436,8 @@ NavMenuInlineTabs.propTypes = {
onToggleFiltersClick: PropTypes.func,
onToggleTagsClick: PropTypes.func,
onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool,
hasActiveTags: PropTypes.bool,
};
function AddBannerButton(props) {
@ -574,7 +600,7 @@ export default function ProfilePagesHeader(props) {
></span>
) : null}
{userCanDeleteProfile ? (
{userCanDeleteProfile && !userIsAuthor ? (
<span className="delete-profile-wrap">
<PopupTrigger contentRef={popupContentRef}>
<button className="delete-profile" title="Remove profile">
@ -602,7 +628,7 @@ export default function ProfilePagesHeader(props) {
</span>
) : null}
{userCanEditProfile ? (
{userCanEditProfile && userIsAuthor ? (
props.author.banner_thumbnail_url ? (
<EditBannerButton link={ProfilePageStore.get('author-data').default_channel_edit_url} />
) : (
@ -620,7 +646,7 @@ export default function ProfilePagesHeader(props) {
{props.author.name ? (
<div className="profile-name-edit-wrapper">
<h1>{props.author.name}</h1>
{userCanEditProfile ? <EditProfileButton link={ProfilePageStore.get('author-data').edit_url} /> : null}
{userCanEditProfile && !userIsAuthor ? <EditProfileButton link={ProfilePageStore.get('author-data').edit_url} /> : null}
</div>
) : null}
</div>
@ -635,6 +661,8 @@ export default function ProfilePagesHeader(props) {
onToggleFiltersClick={props.onToggleFiltersClick}
onToggleTagsClick={props.onToggleTagsClick}
onToggleSortingClick={props.onToggleSortingClick}
hasActiveFilters={props.hasActiveFilters}
hasActiveTags={props.hasActiveTags}
/>
</div>
</div>
@ -648,6 +676,8 @@ 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: sortByFilter,
tag: tagFilter,
sort_by: props.selectedSort || sortByFilter,
tag: props.selectedTag || tagFilter,
};
switch (ev.currentTarget.getAttribute('filter')) {
@ -180,6 +180,8 @@ 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">
<div className="mi-filter mi-filter-full-width">
<div className="mi-filter-title">{translateString('TAGS')}</div>
<div className="mi-filter-options" style={tagsOptions.length >= 10 ? { maxHeight: '300px', overflowY: 'auto' } : {}}>
<div className="mi-filter-options mi-filter-options-horizontal">
<FilterOptions id={'tag'} options={tagsOptions} selected={tagFilter} onSelect={onFilterSelect} />
</div>
</div>

View File

@ -583,17 +583,23 @@ 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,
});
}
@ -869,6 +875,23 @@ 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
@ -879,20 +902,14 @@ 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={
!isMediaAuthor
? this.state.title
: 0 < this.state.channelMediaCount
? this.state.title === 'Uploads'
? null
: this.state.title
: null
}
title={this.state.title}
className="items-list-ver"
showBulkActions={isMediaAuthor}
selectedCount={this.state.selectedMedia.size}
@ -901,7 +918,7 @@ export class ProfileMediaPage extends Page {
onSelectAll={this.handleSelectAll}
onDeselectAll={this.handleDeselectAll}
>
<ProfileMediaFilters hidden={this.state.hiddenFilters} tags={this.state.availableTags} onFiltersUpdate={this.onFiltersUpdate} />
<ProfileMediaFilters hidden={this.state.hiddenFilters} tags={this.state.availableTags} onFiltersUpdate={this.onFiltersUpdate} selectedTag={this.state.selectedTag} selectedSort={this.state.selectedSort} />
<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.link} /> : null;
return props.showSelection ? <MediaItemViewLink link={props.publishLink || 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,6 +12,146 @@
{{ 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,6 +102,7 @@ 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("/")
@ -114,7 +115,13 @@ def edit_user(request, username):
return HttpResponseRedirect(user.get_absolute_url())
else:
form = UserForm(request.user, instance=user)
return render(request, "cms/user_edit.html", {"form": form})
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)
def view_channel(request, friendly_token):