Compare commits

...

9 Commits

Author SHA1 Message Date
Markos Gogoulos
e0f8e839cf fix 2025-10-25 22:32:37 +03:00
Markos Gogoulos
42d65ed4e4 fix 2025-10-25 22:30:29 +03:00
Markos Gogoulos
14de46f8d3 fix 2025-10-25 22:10:25 +03:00
Markos Gogoulos
29d939c47c fix 2025-10-25 22:04:03 +03:00
Markos Gogoulos
9ccd0fa44e fix 2025-10-25 19:25:16 +03:00
Markos Gogoulos
7fa605ff6b fix 2025-10-25 19:21:18 +03:00
Markos Gogoulos
255d004ecb fix 2025-10-25 19:09:55 +03:00
Markos Gogoulos
f701872d39 wtv 2025-10-25 19:09:42 +03:00
Markos Gogoulos
891676243e new logo 2025-10-25 17:58:55 +03:00
38 changed files with 891 additions and 81 deletions

View File

@ -4,6 +4,8 @@ import tempfile
import pysubs2
from django.conf import settings
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.urls import reverse
from .. import helpers
@ -100,3 +102,13 @@ class TranscriptionRequest(models.Model):
def __str__(self):
return f"Transcription request for {self.media.title} - {self.status}"
@receiver(post_save, sender=Subtitle)
def subtitle_save(sender, instance, created, **kwargs):
from .. import tasks
tasks.update_search_vector.apply_async(
args=[instance.media.friendly_token],
countdown=10,
)

View File

@ -528,6 +528,17 @@ def whisper_transcribe(friendly_token, translate_to_english=False):
return False
@task(name="update_search_vector", queue="short_tasks")
def update_search_vector(friendly_token):
try:
media = Media.objects.get(friendly_token=friendly_token)
media.update_search_vector()
except: # noqa
return False
return True
@task(name="produce_sprite_from_video", queue="long_tasks")
def produce_sprite_from_video(friendly_token):
"""Produces a sprites file for a video, uses ffmpeg"""

View File

@ -96,7 +96,7 @@ class MediaList(APIView):
rbac_conditions &= Q(user=user)
conditions |= rbac_conditions
return base_queryset.filter(conditions).distinct()[:1000]
return base_queryset.filter(conditions).distinct()
def get(self, request, format=None):
# Show media
@ -116,6 +116,7 @@ class MediaList(APIView):
upload_date = params.get('upload_date', '').strip()
duration = params.get('duration', '').strip()
publish_state = params.get('publish_state', '').strip()
query = params.get("q", "").strip().lower()
# Handle combined sort options (e.g., title_asc, views_desc)
parsed_combined = False
@ -168,7 +169,7 @@ class MediaList(APIView):
if not self.request.user.is_authenticated:
media = Media.objects.none()
else:
media = Media.objects.filter(permissions__owner_user=self.request.user).prefetch_related("user", "tags")
media = Media.objects.filter(permissions__owner_user=self.request.user).prefetch_related("user", "tags").distinct()
elif show_param == "shared_with_me":
if not self.request.user.is_authenticated:
media = Media.objects.none()
@ -221,11 +222,13 @@ class MediaList(APIView):
if publish_state and publish_state in ['private', 'public', 'unlisted']:
media = media.filter(state=publish_state)
if query:
media = media.filter(title__icontains=query)
if not already_sorted:
media = media.order_by(f"{ordering}{sort_by}")
if show_param == "shared_with_me":
media = media[:1000] # limit to 1000 results
media = media[:1000] # limit to 1000 results
paginator = pagination_class()

View File

@ -232,7 +232,7 @@
}
}
.playlist-item {
.playlist-list .playlist-item {
display: flex;
justify-content: space-between;
align-items: center;

View File

@ -0,0 +1,201 @@
import React from 'react';
import PropTypes from 'prop-types';
import { BulkActionConfirmModal } from './BulkActionConfirmModal';
import { BulkActionPermissionModal } from './BulkActionPermissionModal';
import { BulkActionPlaylistModal } from './BulkActionPlaylistModal';
import { BulkActionChangeOwnerModal } from './BulkActionChangeOwnerModal';
import { BulkActionPublishStateModal } from './BulkActionPublishStateModal';
import { BulkActionCategoryModal } from './BulkActionCategoryModal';
import { BulkActionTagModal } from './BulkActionTagModal';
/**
* Renders all bulk action modals
* This component is reusable across different pages
*/
export function BulkActionsModals({
// Confirm modal props
showConfirmModal,
confirmMessage,
onConfirmCancel,
onConfirmProceed,
// Permission modal props
showPermissionModal,
permissionType,
selectedMediaIds,
onPermissionModalCancel,
onPermissionModalSuccess,
onPermissionModalError,
// Playlist modal props
showPlaylistModal,
onPlaylistModalCancel,
onPlaylistModalSuccess,
onPlaylistModalError,
username,
// Change owner modal props
showChangeOwnerModal,
onChangeOwnerModalCancel,
onChangeOwnerModalSuccess,
onChangeOwnerModalError,
// Publish state modal props
showPublishStateModal,
onPublishStateModalCancel,
onPublishStateModalSuccess,
onPublishStateModalError,
// Category modal props
showCategoryModal,
onCategoryModalCancel,
onCategoryModalSuccess,
onCategoryModalError,
// Tag modal props
showTagModal,
onTagModalCancel,
onTagModalSuccess,
onTagModalError,
// Common props
csrfToken,
// Notification
showNotification,
notificationMessage,
notificationType,
}) {
return (
<>
<BulkActionConfirmModal
isOpen={showConfirmModal}
message={confirmMessage}
onCancel={onConfirmCancel}
onProceed={onConfirmProceed}
/>
<BulkActionPermissionModal
isOpen={showPermissionModal}
permissionType={permissionType}
selectedMediaIds={selectedMediaIds}
onCancel={onPermissionModalCancel}
onSuccess={onPermissionModalSuccess}
onError={onPermissionModalError}
csrfToken={csrfToken}
/>
<BulkActionPlaylistModal
isOpen={showPlaylistModal}
selectedMediaIds={selectedMediaIds}
onCancel={onPlaylistModalCancel}
onSuccess={onPlaylistModalSuccess}
onError={onPlaylistModalError}
csrfToken={csrfToken}
username={username}
/>
<BulkActionChangeOwnerModal
isOpen={showChangeOwnerModal}
selectedMediaIds={selectedMediaIds}
onCancel={onChangeOwnerModalCancel}
onSuccess={onChangeOwnerModalSuccess}
onError={onChangeOwnerModalError}
csrfToken={csrfToken}
/>
<BulkActionPublishStateModal
isOpen={showPublishStateModal}
selectedMediaIds={selectedMediaIds}
onCancel={onPublishStateModalCancel}
onSuccess={onPublishStateModalSuccess}
onError={onPublishStateModalError}
csrfToken={csrfToken}
/>
<BulkActionCategoryModal
isOpen={showCategoryModal}
selectedMediaIds={selectedMediaIds}
onCancel={onCategoryModalCancel}
onSuccess={onCategoryModalSuccess}
onError={onCategoryModalError}
csrfToken={csrfToken}
/>
<BulkActionTagModal
isOpen={showTagModal}
selectedMediaIds={selectedMediaIds}
onCancel={onTagModalCancel}
onSuccess={onTagModalSuccess}
onError={onTagModalError}
csrfToken={csrfToken}
/>
{showNotification && (
<div
style={{
position: 'fixed',
bottom: '20px',
left: '260px',
backgroundColor: notificationType === 'error' ? '#f44336' : '#4CAF50',
color: 'white',
padding: '16px 24px',
borderRadius: '4px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
zIndex: 1000,
fontSize: '14px',
fontWeight: '500',
}}
>
{notificationMessage}
</div>
)}
</>
);
}
BulkActionsModals.propTypes = {
showConfirmModal: PropTypes.bool.isRequired,
confirmMessage: PropTypes.string.isRequired,
onConfirmCancel: PropTypes.func.isRequired,
onConfirmProceed: PropTypes.func.isRequired,
showPermissionModal: PropTypes.bool.isRequired,
permissionType: PropTypes.oneOf(['viewer', 'editor', 'owner', null]),
selectedMediaIds: PropTypes.array.isRequired,
onPermissionModalCancel: PropTypes.func.isRequired,
onPermissionModalSuccess: PropTypes.func.isRequired,
onPermissionModalError: PropTypes.func.isRequired,
showPlaylistModal: PropTypes.bool.isRequired,
onPlaylistModalCancel: PropTypes.func.isRequired,
onPlaylistModalSuccess: PropTypes.func.isRequired,
onPlaylistModalError: PropTypes.func.isRequired,
username: PropTypes.string,
showChangeOwnerModal: PropTypes.bool.isRequired,
onChangeOwnerModalCancel: PropTypes.func.isRequired,
onChangeOwnerModalSuccess: PropTypes.func.isRequired,
onChangeOwnerModalError: PropTypes.func.isRequired,
showPublishStateModal: PropTypes.bool.isRequired,
onPublishStateModalCancel: PropTypes.func.isRequired,
onPublishStateModalSuccess: PropTypes.func.isRequired,
onPublishStateModalError: PropTypes.func.isRequired,
showCategoryModal: PropTypes.bool.isRequired,
onCategoryModalCancel: PropTypes.func.isRequired,
onCategoryModalSuccess: PropTypes.func.isRequired,
onCategoryModalError: PropTypes.func.isRequired,
showTagModal: PropTypes.bool.isRequired,
onTagModalCancel: PropTypes.func.isRequired,
onTagModalSuccess: PropTypes.func.isRequired,
onTagModalError: PropTypes.func.isRequired,
csrfToken: PropTypes.string.isRequired,
showNotification: PropTypes.bool.isRequired,
notificationMessage: PropTypes.string.isRequired,
notificationType: PropTypes.oneOf(['success', 'error']).isRequired,
};

View File

@ -45,10 +45,11 @@ class ProfileSearchBar extends React.PureComponent {
onChange(ev) {
this.pendingEvent = ev;
const newValue = ev.target.value || '';
this.setState(
{
queryVal: ev.target.value || '',
queryVal: newValue,
},
function () {
if (this.updateTimeout) {
@ -57,8 +58,11 @@ class ProfileSearchBar extends React.PureComponent {
this.pendingEvent = null;
// Only trigger search if 3+ characters or empty (to reset)
if ('function' === typeof this.props.onQueryChange) {
this.props.onQueryChange(this.state.queryVal);
if (newValue.length >= 3 || newValue.length === 0) {
this.props.onQueryChange(newValue);
}
}
this.updateTimeout = setTimeout(
@ -137,24 +141,56 @@ class ProfileSearchBar extends React.PureComponent {
}
render() {
const hasSearchText = this.state.queryVal && this.state.queryVal.length > 0;
// Determine the correct action URL based on page type
let actionUrl = LinksContext._currentValue.profile.media;
if (this.props.type === 'shared_by_me') {
actionUrl = LinksContext._currentValue.profile.shared_by_me;
} else if (this.props.type === 'shared_with_me') {
actionUrl = LinksContext._currentValue.profile.shared_with_me;
}
if (!this.state.visibleForm) {
return (
<div>
<span>
<CircleIconButton buttonShadow={false} onClick={this.showForm}>
<i className="material-icons">search</i>
</CircleIconButton>
</span>
</div>
<span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', position: 'relative' }} onClick={this.showForm}>
<CircleIconButton buttonShadow={false}>
<i className="material-icons">search</i>
</CircleIconButton>
{hasSearchText ? (
<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>
);
}
return (
<form method="get" action={LinksContext._currentValue.profile.media} onSubmit={this.onFormSubmit}>
<span>
<form method="get" action={actionUrl} onSubmit={this.onFormSubmit}>
<span style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
<CircleIconButton buttonShadow={false}>
<i className="material-icons">search</i>
</CircleIconButton>
{hasSearchText ? (
<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>
<input
@ -177,6 +213,7 @@ class ProfileSearchBar extends React.PureComponent {
ProfileSearchBar.propTypes = {
onQueryChange: PropTypes.func,
type: PropTypes.string,
};
ProfileSearchBar.defaultProps = {};
@ -368,7 +405,7 @@ class NavMenuInlineTabs extends React.PureComponent {
) : null}
<li className="media-search">
<ProfileSearchBar onQueryChange={this.props.onQueryChange} toggleSearchField={this.onToggleSearchField} />
<ProfileSearchBar onQueryChange={this.props.onQueryChange} toggleSearchField={this.onToggleSearchField} type={this.props.type} />
</li>
{this.props.onToggleFiltersClick && ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<li className="media-filters-toggle">

View File

@ -23,24 +23,6 @@ import { Page } from './_Page';
import '../components/profile-page/ProfilePage.scss';
function EmptyChannelMedia(props) {
return (
<LinksConsumer>
{(links) => (
<div className="empty-media empty-channel-media">
<div className="welcome-title">{translateString('Welcome')} {props.name}</div>
<div className="start-uploading">
{translateString('Start uploading media and sharing your work. Media that you upload will show up here.')}
</div>
<a href={links.user.addMedia} title={translateString('Upload media')} className="button-link">
<i className="material-icons" data-icon="video_call"></i>{translateString('UPLOAD MEDIA')}
</a>
</div>
)}
</LinksConsumer>
);
}
export class ProfileMediaPage extends Page {
constructor(props, pageSlug) {
super(props, 'string' === typeof pageSlug ? pageSlug : 'author-home');
@ -138,7 +120,7 @@ export class ProfileMediaPage extends Page {
if (author) {
if (this.state.query) {
requestUrl = ApiUrlContext._currentValue.search.query + this.state.query + '&author=' + author.id + this.state.filterArgs;
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs;
} else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + this.state.filterArgs;
}
@ -189,7 +171,7 @@ export class ProfileMediaPage extends Page {
let requestUrl;
if (newQuery) {
requestUrl = ApiUrlContext._currentValue.search.query + newQuery + '&author=' + this.state.author.id + this.state.filterArgs;
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&q=' + encodeURIComponent(newQuery) + this.state.filterArgs;
} else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + this.state.filterArgs;
}
@ -721,7 +703,7 @@ export class ProfileMediaPage extends Page {
let requestUrl;
if (this.state.query) {
requestUrl = ApiUrlContext._currentValue.search.query + this.state.query + '&author=' + this.state.author.id + this.state.filterArgs;
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs;
} else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + this.state.filterArgs;
}
@ -940,9 +922,6 @@ export class ProfileMediaPage extends Page {
onItemsUpdate={this.handleItemsUpdate}
onResponseDataLoaded={this.onResponseDataLoaded}
/>
{isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
<EmptyChannelMedia name={this.state.author.name} />
) : null}
</MediaListWrapper>
</ProfilePagesContent>
) : null,

View File

@ -10,7 +10,9 @@ import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListA
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
import { BulkActionsModals } from '../components/BulkActionsModals';
import { translateString } from '../utils/helpers';
import { withBulkActions } from '../utils/hoc/withBulkActions';
import { Page } from './_Page';
@ -31,7 +33,7 @@ function EmptySharedByMe(props) {
);
}
export class ProfileSharedByMePage extends Page {
class ProfileSharedByMePage extends Page {
constructor(props, pageSlug) {
super(props, 'string' === typeof pageSlug ? pageSlug : 'author-shared-by-me');
@ -79,7 +81,7 @@ export class ProfileSharedByMePage extends Page {
if (author) {
if (this.state.query) {
requestUrl = ApiUrlContext._currentValue.search.query + this.state.query + '&author=' + author.id + '&show=shared_by_me' + this.state.filterArgs;
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_by_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs;
} else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_by_me' + this.state.filterArgs;
}
@ -130,7 +132,7 @@ export class ProfileSharedByMePage extends Page {
let requestUrl;
if (newQuery) {
requestUrl = ApiUrlContext._currentValue.search.query + newQuery + '&author=' + this.state.author.id + '&show=shared_by_me' + this.state.filterArgs;
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me&q=' + encodeURIComponent(newQuery) + this.state.filterArgs;
} else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me' + this.state.filterArgs;
}
@ -272,7 +274,7 @@ export class ProfileSharedByMePage extends Page {
let requestUrl;
if (this.state.query) {
requestUrl = ApiUrlContext._currentValue.search.query + this.state.query + '&author=' + this.state.author.id + '&show=shared_by_me' + this.state.filterArgs;
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs;
} else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me' + this.state.filterArgs;
}
@ -320,14 +322,20 @@ export class ProfileSharedByMePage extends Page {
this.state.author ? (
<ProfilePagesContent key="ProfilePagesContent">
<MediaListWrapper
title={!isMediaAuthor || 0 < this.state.channelMediaCount ? this.state.title : null}
title={this.state.title}
className="items-list-ver"
showBulkActions={isMediaAuthor}
selectedCount={this.props.bulkActions.selectedMedia.size}
totalCount={this.props.bulkActions.availableMediaIds.length}
onBulkAction={this.props.bulkActions.handleBulkAction}
onSelectAll={this.props.bulkActions.handleSelectAll}
onDeselectAll={this.props.bulkActions.handleDeselectAll}
>
<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
key={this.state.requestUrl}
key={`${this.state.requestUrl}-${this.props.bulkActions.listKey}`}
requestUrl={this.state.requestUrl}
hideAuthor={true}
itemsCountCallback={this.state.requestUrl ? this.getCountFunc : null}
@ -335,6 +343,11 @@ export class ProfileSharedByMePage extends Page {
hideDate={!PageStore.get('config-media-item').displayPublishDate}
canEdit={false}
onResponseDataLoaded={this.onResponseDataLoaded}
showSelection={isMediaAuthor}
hasAnySelection={this.props.bulkActions.selectedMedia.size > 0}
selectedMedia={this.props.bulkActions.selectedMedia}
onMediaSelection={this.props.bulkActions.handleMediaSelection}
onItemsUpdate={this.props.bulkActions.handleItemsUpdate}
/>
{isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
<EmptySharedByMe name={this.state.author.name} />
@ -342,14 +355,51 @@ export class ProfileSharedByMePage extends Page {
</MediaListWrapper>
</ProfilePagesContent>
) : null,
this.state.author && isMediaAuthor ? (
<BulkActionsModals
key="BulkActionsModals"
{...this.props.bulkActions}
selectedMediaIds={Array.from(this.props.bulkActions.selectedMedia)}
csrfToken={this.props.bulkActions.getCsrfToken()}
username={this.state.author.username}
onConfirmCancel={this.props.bulkActions.handleConfirmCancel}
onConfirmProceed={this.props.bulkActions.handleConfirmProceed}
onPermissionModalCancel={this.props.bulkActions.handlePermissionModalCancel}
onPermissionModalSuccess={this.props.bulkActions.handlePermissionModalSuccess}
onPermissionModalError={this.props.bulkActions.handlePermissionModalError}
onPlaylistModalCancel={this.props.bulkActions.handlePlaylistModalCancel}
onPlaylistModalSuccess={this.props.bulkActions.handlePlaylistModalSuccess}
onPlaylistModalError={this.props.bulkActions.handlePlaylistModalError}
onChangeOwnerModalCancel={this.props.bulkActions.handleChangeOwnerModalCancel}
onChangeOwnerModalSuccess={this.props.bulkActions.handleChangeOwnerModalSuccess}
onChangeOwnerModalError={this.props.bulkActions.handleChangeOwnerModalError}
onPublishStateModalCancel={this.props.bulkActions.handlePublishStateModalCancel}
onPublishStateModalSuccess={this.props.bulkActions.handlePublishStateModalSuccess}
onPublishStateModalError={this.props.bulkActions.handlePublishStateModalError}
onCategoryModalCancel={this.props.bulkActions.handleCategoryModalCancel}
onCategoryModalSuccess={this.props.bulkActions.handleCategoryModalSuccess}
onCategoryModalError={this.props.bulkActions.handleCategoryModalError}
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
onTagModalError={this.props.bulkActions.handleTagModalError}
/>
) : null,
];
}
}
ProfileSharedByMePage.propTypes = {
title: PropTypes.string.isRequired,
bulkActions: PropTypes.object.isRequired,
};
ProfileSharedByMePage.defaultProps = {
title: 'Shared by me',
};
// Wrap with HOC and export as named export for compatibility
const WrappedProfileSharedByMePage = withBulkActions(ProfileSharedByMePage);
// Export both the wrapped component as named export (for build system) and default export
export { WrappedProfileSharedByMePage as ProfileSharedByMePage };
export default WrappedProfileSharedByMePage;

View File

@ -79,7 +79,7 @@ export class ProfileSharedWithMePage extends Page {
if (author) {
if (this.state.query) {
requestUrl = ApiUrlContext._currentValue.search.query + this.state.query + '&author=' + author.id + '&show=shared_with_me' + this.state.filterArgs;
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_with_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs;
} else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_with_me' + this.state.filterArgs;
}
@ -130,7 +130,7 @@ export class ProfileSharedWithMePage extends Page {
let requestUrl;
if (newQuery) {
requestUrl = ApiUrlContext._currentValue.search.query + newQuery + '&author=' + this.state.author.id + '&show=shared_with_me' + this.state.filterArgs;
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me&q=' + encodeURIComponent(newQuery) + this.state.filterArgs;
} else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me' + this.state.filterArgs;
}
@ -272,7 +272,7 @@ export class ProfileSharedWithMePage extends Page {
let requestUrl;
if (this.state.query) {
requestUrl = ApiUrlContext._currentValue.search.query + this.state.query + '&author=' + this.state.author.id + '&show=shared_with_me' + this.state.filterArgs;
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs;
} else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me' + this.state.filterArgs;
}
@ -320,7 +320,7 @@ export class ProfileSharedWithMePage extends Page {
this.state.author ? (
<ProfilePagesContent key="ProfilePagesContent">
<MediaListWrapper
title={!isMediaAuthor || 0 < this.state.channelMediaCount ? this.state.title : null}
title={this.state.title}
className="items-list-ver"
>
<ProfileMediaFilters hidden={this.state.hiddenFilters} tags={this.state.availableTags} onFiltersUpdate={this.onFiltersUpdate} />

View File

@ -10,3 +10,4 @@ export * from './useMediaItem';
export * from './usePopup';
export * from './useTheme';
export * from './useUser';
export * from './useBulkActions';

View File

@ -0,0 +1,516 @@
import { useState } from 'react';
import { translateString } from '../helpers';
/**
* Custom hook for managing bulk actions on media items
* Provides state management and handlers for selecting media and executing bulk actions
*/
export function useBulkActions() {
const [selectedMedia, setSelectedMedia] = useState(new Set());
const [availableMediaIds, setAvailableMediaIds] = useState([]);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [pendingAction, setPendingAction] = useState(null);
const [confirmMessage, setConfirmMessage] = useState('');
const [listKey, setListKey] = useState(0);
const [notificationMessage, setNotificationMessage] = useState('');
const [showNotification, setShowNotification] = useState(false);
const [notificationType, setNotificationType] = useState('success');
const [showPermissionModal, setShowPermissionModal] = useState(false);
const [permissionType, setPermissionType] = useState(null);
const [showPlaylistModal, setShowPlaylistModal] = useState(false);
const [showChangeOwnerModal, setShowChangeOwnerModal] = useState(false);
const [showPublishStateModal, setShowPublishStateModal] = useState(false);
const [showCategoryModal, setShowCategoryModal] = useState(false);
const [showTagModal, setShowTagModal] = useState(false);
// Get CSRF token from cookies
const getCsrfToken = () => {
const name = 'csrftoken';
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === name + '=') {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
};
// Show notification
const showNotificationMessage = (message, type = 'success') => {
setNotificationMessage(message);
setShowNotification(true);
setNotificationType(type);
setTimeout(() => {
setShowNotification(false);
}, 5000);
};
// Handle media selection toggle
const handleMediaSelection = (mediaId, isSelected) => {
setSelectedMedia((prevState) => {
const newSelectedMedia = new Set(prevState);
if (isSelected) {
newSelectedMedia.add(mediaId);
} else {
newSelectedMedia.delete(mediaId);
}
return newSelectedMedia;
});
};
// Handle items update from list
const handleItemsUpdate = (items) => {
const mediaIds = items.map((item) => item.friendly_token || item.uid || item.id);
setAvailableMediaIds(mediaIds);
};
// Select all available media
const handleSelectAll = () => {
setSelectedMedia(new Set(availableMediaIds));
};
// Deselect all media
const handleDeselectAll = () => {
setSelectedMedia(new Set());
};
// Clear selection
const clearSelection = () => {
setSelectedMedia(new Set());
};
// Clear selection and refresh list
const clearSelectionAndRefresh = () => {
setSelectedMedia(new Set());
setListKey((prev) => prev + 1);
};
// Handle bulk action button clicks
const handleBulkAction = (action) => {
const selectedCount = selectedMedia.size;
if (selectedCount === 0) {
return;
}
if (action === 'delete-media') {
setShowConfirmModal(true);
setPendingAction(action);
setConfirmMessage(translateString('You are going to delete') + ` ${selectedCount} ` + translateString('media, are you sure?'));
} else if (action === 'enable-comments') {
setShowConfirmModal(true);
setPendingAction(action);
setConfirmMessage(translateString('You are going to enable comments to') + ` ${selectedCount} ` + translateString('media, are you sure?'));
} else if (action === 'disable-comments') {
setShowConfirmModal(true);
setPendingAction(action);
setConfirmMessage(translateString('You are going to disable comments to') + ` ${selectedCount} ` + translateString('media, are you sure?'));
} else if (action === 'enable-download') {
setShowConfirmModal(true);
setPendingAction(action);
setConfirmMessage(translateString('You are going to enable download for') + ` ${selectedCount} ` + translateString('media, are you sure?'));
} else if (action === 'disable-download') {
setShowConfirmModal(true);
setPendingAction(action);
setConfirmMessage(translateString('You are going to disable download for') + ` ${selectedCount} ` + translateString('media, are you sure?'));
} else if (action === 'copy-media') {
setShowConfirmModal(true);
setPendingAction(action);
setConfirmMessage(translateString('You are going to copy') + ` ${selectedCount} ` + translateString('media, are you sure?'));
} else if (action === 'add-remove-coviewers') {
setShowPermissionModal(true);
setPermissionType('viewer');
} else if (action === 'add-remove-coeditors') {
setShowPermissionModal(true);
setPermissionType('editor');
} else if (action === 'add-remove-coowners') {
setShowPermissionModal(true);
setPermissionType('owner');
} else if (action === 'add-remove-playlist') {
setShowPlaylistModal(true);
} else if (action === 'change-owner') {
setShowChangeOwnerModal(true);
} else if (action === 'publish-state') {
setShowPublishStateModal(true);
} else if (action === 'add-remove-category') {
setShowCategoryModal(true);
} else if (action === 'add-remove-tags') {
setShowTagModal(true);
}
};
// Cancel confirm modal
const handleConfirmCancel = () => {
setShowConfirmModal(false);
setPendingAction(null);
setConfirmMessage('');
};
// Proceed with confirmed action
const handleConfirmProceed = () => {
const action = pendingAction;
setShowConfirmModal(false);
setPendingAction(null);
setConfirmMessage('');
if (action === 'delete-media') {
executeDeleteMedia();
} else if (action === 'enable-comments') {
executeEnableComments();
} else if (action === 'disable-comments') {
executeDisableComments();
} else if (action === 'enable-download') {
executeEnableDownload();
} else if (action === 'disable-download') {
executeDisableDownload();
} else if (action === 'copy-media') {
executeCopyMedia();
}
};
// Execute delete media
const executeDeleteMedia = () => {
const selectedIds = Array.from(selectedMedia);
const selectedCount = selectedIds.length;
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken(),
},
body: JSON.stringify({
action: 'delete_media',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to delete media');
}
return response.json();
})
.then((data) => {
const message = selectedCount === 1
? translateString('The media was deleted successfully.')
: translateString('Successfully deleted') + ` ${selectedCount} ` + translateString('media.');
showNotificationMessage(message);
clearSelectionAndRefresh();
})
.catch((error) => {
showNotificationMessage(translateString('Failed to delete media. Please try again.'), 'error');
clearSelectionAndRefresh();
});
};
// Execute enable comments
const executeEnableComments = () => {
const selectedIds = Array.from(selectedMedia);
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken(),
},
body: JSON.stringify({
action: 'enable_comments',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to enable comments');
}
return response.json();
})
.then((data) => {
showNotificationMessage(translateString('Successfully Enabled comments'));
clearSelection();
})
.catch((error) => {
showNotificationMessage(translateString('Failed to enable comments.'), 'error');
clearSelection();
});
};
// Execute disable comments
const executeDisableComments = () => {
const selectedIds = Array.from(selectedMedia);
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken(),
},
body: JSON.stringify({
action: 'disable_comments',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to disable comments');
}
return response.json();
})
.then((data) => {
showNotificationMessage(translateString('Successfully Disabled comments'));
clearSelection();
})
.catch((error) => {
showNotificationMessage(translateString('Failed to disable comments.'), 'error');
clearSelection();
});
};
// Execute enable download
const executeEnableDownload = () => {
const selectedIds = Array.from(selectedMedia);
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken(),
},
body: JSON.stringify({
action: 'enable_download',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to enable download');
}
return response.json();
})
.then((data) => {
showNotificationMessage(translateString('Successfully Enabled Download'));
clearSelection();
})
.catch((error) => {
showNotificationMessage(translateString('Failed to enable download.'), 'error');
clearSelection();
});
};
// Execute disable download
const executeDisableDownload = () => {
const selectedIds = Array.from(selectedMedia);
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken(),
},
body: JSON.stringify({
action: 'disable_download',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to disable download');
}
return response.json();
})
.then((data) => {
showNotificationMessage(translateString('Successfully Disabled Download'));
clearSelection();
})
.catch((error) => {
showNotificationMessage(translateString('Failed to disable download.'), 'error');
clearSelection();
});
};
// Execute copy media
const executeCopyMedia = () => {
const selectedIds = Array.from(selectedMedia);
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken(),
},
body: JSON.stringify({
action: 'copy_media',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to copy media');
}
return response.json();
})
.then((data) => {
showNotificationMessage(translateString('Successfully Copied'));
clearSelectionAndRefresh();
})
.catch((error) => {
showNotificationMessage(translateString('Failed to copy media.'), 'error');
clearSelection();
});
};
// Permission modal handlers
const handlePermissionModalCancel = () => {
setShowPermissionModal(false);
setPermissionType(null);
};
const handlePermissionModalSuccess = (message) => {
showNotificationMessage(message);
clearSelection();
setShowPermissionModal(false);
setPermissionType(null);
};
const handlePermissionModalError = (message) => {
showNotificationMessage(message, 'error');
setShowPermissionModal(false);
setPermissionType(null);
};
// Playlist modal handlers
const handlePlaylistModalCancel = () => {
setShowPlaylistModal(false);
};
const handlePlaylistModalSuccess = (message) => {
showNotificationMessage(message);
clearSelection();
setShowPlaylistModal(false);
};
const handlePlaylistModalError = (message) => {
showNotificationMessage(message, 'error');
setShowPlaylistModal(false);
};
// Change owner modal handlers
const handleChangeOwnerModalCancel = () => {
setShowChangeOwnerModal(false);
};
const handleChangeOwnerModalSuccess = (message) => {
showNotificationMessage(message);
clearSelectionAndRefresh();
setShowChangeOwnerModal(false);
};
const handleChangeOwnerModalError = (message) => {
showNotificationMessage(message, 'error');
setShowChangeOwnerModal(false);
};
// Publish state modal handlers
const handlePublishStateModalCancel = () => {
setShowPublishStateModal(false);
};
const handlePublishStateModalSuccess = (message) => {
showNotificationMessage(message);
clearSelectionAndRefresh();
setShowPublishStateModal(false);
};
const handlePublishStateModalError = (message) => {
showNotificationMessage(message, 'error');
setShowPublishStateModal(false);
};
// Category modal handlers
const handleCategoryModalCancel = () => {
setShowCategoryModal(false);
};
const handleCategoryModalSuccess = (message) => {
showNotificationMessage(message);
clearSelection();
setShowCategoryModal(false);
};
const handleCategoryModalError = (message) => {
showNotificationMessage(message, 'error');
setShowCategoryModal(false);
};
// Tag modal handlers
const handleTagModalCancel = () => {
setShowTagModal(false);
};
const handleTagModalSuccess = (message) => {
showNotificationMessage(message);
clearSelection();
setShowTagModal(false);
};
const handleTagModalError = (message) => {
showNotificationMessage(message, 'error');
setShowTagModal(false);
};
return {
// State
selectedMedia,
availableMediaIds,
listKey,
showConfirmModal,
confirmMessage,
notificationMessage,
showNotification,
notificationType,
showPermissionModal,
permissionType,
showPlaylistModal,
showChangeOwnerModal,
showPublishStateModal,
showCategoryModal,
showTagModal,
// Handlers
handleMediaSelection,
handleItemsUpdate,
handleSelectAll,
handleDeselectAll,
handleBulkAction,
handleConfirmCancel,
handleConfirmProceed,
handlePermissionModalCancel,
handlePermissionModalSuccess,
handlePermissionModalError,
handlePlaylistModalCancel,
handlePlaylistModalSuccess,
handlePlaylistModalError,
handleChangeOwnerModalCancel,
handleChangeOwnerModalSuccess,
handleChangeOwnerModalError,
handlePublishStateModalCancel,
handlePublishStateModalSuccess,
handlePublishStateModalError,
handleCategoryModalCancel,
handleCategoryModalSuccess,
handleCategoryModalError,
handleTagModalCancel,
handleTagModalSuccess,
handleTagModalError,
// Utility
getCsrfToken,
clearSelection,
clearSelectionAndRefresh,
};
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 85 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long